PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / Payments / Components / Subscription.php

Subscription.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Modules/Payments/Components/Subscription.php

529 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\Payments\Components;
4
5 if (!defined('ABSPATH')) {
6 exit; // Exit if accessed directly.
7 }
8
9 use FluentForm\App\Helpers\Helper;
10 use FluentForm\App\Modules\Payments\PaymentHelper;
11 use FluentForm\Framework\Helpers\ArrayHelper;
12 use FluentForm\App\Modules\Form\FormFieldsParser;
13 use FluentForm\App\Services\FormBuilder\BaseFieldManager;
14
15 class Subscription extends BaseFieldManager
16 {
17 public function __construct(
18 $key = 'subscription_payment_component',
19 $title = 'Subscription Field',
20 $tags = ['custom', 'payment', 'donation', 'subscription'],
21 $position = 'payments'
22 )
23 {
24 parent::__construct(
25 $key,
26 $title,
27 $tags,
28 $position
29 );
30
31 add_filter('fluentform/editor_init_element_subscription_payment_component', function ($element) {
32 if (!isset($element['settings']['layout_class'])) {
33 $element['settings']['layout_class'] = '';
34 }
35 // Backward compatibility: ensure existing plans have end date fields
36 if (!empty($element['settings']['subscription_options'])) {
37 foreach ($element['settings']['subscription_options'] as &$option) {
38 if (!isset($option['has_end_date'])) {
39 $option['has_end_date'] = 'no';
40 }
41 if (!isset($option['subscription_end_date'])) {
42 $option['subscription_end_date'] = '';
43 }
44 if (!isset($option['expire_behavior'])) {
45 $option['expire_behavior'] = 'show_error';
46 }
47 }
48 unset($option);
49 }
50 return $element;
51 });
52 add_filter('fluentform/response_render_' . $this->key, function ($response, $field, $form_id, $isHtml = false) {
53 if (!$isHtml) {
54 return $response;
55 }
56 return fluentform_sanitize_html(
57 ArrayHelper::get($field, 'raw.settings.subscription_options.' . $response . '.name', $response)
58 );
59 }, 10, 4);
60 add_filter('fluentform/white_listed_fields', [$this, 'addWhiteListedFields'], 10, 2);
61 }
62
63 function getComponent()
64 {
65 return array(
66 'index' => 8,
67 'element' => $this->key,
68 'attributes' => array(
69 'type' => 'single', // single|multiple
70 'name' => 'payment_input',
71 'value' => '10'
72 ),
73 'settings' => array(
74 'container_class' => '',
75 'label' => __('Subscription Item', 'fluentform'),
76 'admin_field_label' => '',
77 'label_placement' => '',
78 'display_type' => '',
79 'help_message' => '',
80 'is_payment_field' => 'yes',
81 'selection_type' => 'radio',
82 'subscription_options' => [
83 [
84 "bill_times" => 0,
85 "billing_interval" => "month",
86 "has_signup_fee" => "no",
87 "has_trial_days" => "no",
88 "is_default" => "yes",
89 "name" => __("Monthly Plan", 'fluentform'),
90 "plan_features" => [],
91 "signup_fee" => 0,
92 "subscription_amount" => 9.99,
93 "trial_days" => 0,
94 "has_end_date" => "no",
95 "subscription_end_date" => "",
96 "expire_behavior" => "show_error",
97 ]
98 ],
99 'price_label' => __('Price:', 'fluentform'),
100 'enable_quantity' => false,
101 'enable_image_input' => false,
102 'is_element_lock' => false,
103 'layout_class' => 'ff_list_buttons',
104 'validation_rules' => array(
105 'required' => array(
106 'value' => false,
107 'global' => true,
108 'message' => Helper::getGlobalDefaultMessage('required'),
109 'global_message' => Helper::getGlobalDefaultMessage('required'),
110 ),
111 ),
112 'conditional_logics' => array(),
113 ),
114 'editor_options' => array(
115 'title' => __('Subscription', 'fluentform'),
116 'icon_class' => 'ff-edit-shopping-cart',
117 'element' => 'input-radio',
118 'template' => 'inputSubscriptionPayment'
119 )
120 );
121 }
122
123 public function getGeneralEditorElements()
124 {
125 return [
126 'label',
127 'label_placement',
128 'admin_field_label',
129 'placeholder',
130 'subscription_options',
131 'validation_rules',
132 ];
133 }
134
135 public function getAdvancedEditorElements()
136 {
137 return [
138 'container_class',
139 'help_message',
140 'name',
141 'layout_class',
142 'conditional_logics'
143 ];
144 }
145
146 function render($data, $form)
147 {
148 $type = ArrayHelper::get($data, 'attributes.type');
149
150 if ($type == 'single') {
151 $this->renderSingleItem($data, $form);
152 return '';
153 }
154
155 $this->renderMultiProduct($data, $form);
156 }
157
158 private function isPlanExpiredAndHidden($plan)
159 {
160 return PaymentHelper::isPlanExpiredAndHidden($plan);
161 }
162
163 public function renderSingleItem($data, $form)
164 {
165 $elementName = $data['element'];
166 $data = apply_filters_deprecated(
167 'fluentform_rendering_field_data_' . $elementName,
168 [
169 $data,
170 $form
171 ],
172 FLUENTFORM_FRAMEWORK_UPGRADE,
173 'fluentform/rendering_field_data_' . $elementName,
174 'Use fluentform/rendering_field_data_' . $elementName . ' instead of fluentform_rendering_field_data_' . $elementName
175 );
176
177 $data = apply_filters('fluentform/rendering_field_data_' . $elementName, $data, $form);
178 $plan = ArrayHelper::get($data, 'settings.subscription_options.0', []);
179 $plan['index'] = 0;
180
181 // Hide the entire field if the single plan is expired and set to hide
182 if ($this->isPlanExpiredAndHidden($plan)) {
183 return;
184 }
185
186 $isCustomAmount = ArrayHelper::get($plan, 'user_input') === 'yes';
187 if ($isCustomAmount) {
188 $plan['subscription_amount'] = ArrayHelper::get($plan, 'user_input_default_value');
189 }
190
191 $inputAttributes = [
192 'type' => 'hidden',
193 'name' => $data['attributes']['name'],
194 'value' => '0',
195 'class' => 'ff_payment_item ff_subscription_item',
196 'data-name' => $data['attributes']['name'],
197 'data-payment_item' => 'yes',
198 ];
199
200 $inputAttributes['id'] = $this->makeElementId($data, $form);
201 $currency = PaymentHelper::getFormCurrency($form->id);
202
203 $billingAttributes = $this->getPlanInputAttributes($plan);
204
205 $itemInputAttributes = wp_parse_args($billingAttributes, $inputAttributes);
206
207 $elMarkup = "<input " . $this->buildAttributes($itemInputAttributes, $form) . ">";
208
209 if ($isCustomAmount) {
210 $plan['user_input_label'] = ArrayHelper::get($plan, 'user_input_label');
211 $plan['user_input_label'] = $plan['user_input_label'] ?: ArrayHelper::get($data, 'settings.label');
212
213 $elMarkup = $this->makeCustomInputHtml($data, $plan, 'hidden', $elMarkup);
214
215 $data['settings']['label'] = ArrayHelper::get($plan, 'user_input_label', $data['settings']['label']);
216 $data['attributes']['id'] = ArrayHelper::get($data, 'attributes.name') . '_custom_' . $plan['index'];
217 }
218
219 $paymentSummary = $this->getPaymentSummaryText($plan, $form, $currency);
220 $elMarkup .= $paymentSummary;
221
222 $html = $this->buildElementMarkup($elMarkup, $data, $form);
223
224 $html = apply_filters_deprecated(
225 'fluentform_rendering_field_html_' . $elementName,
226 [
227 $html,
228 $data,
229 $form
230 ],
231 FLUENTFORM_FRAMEWORK_UPGRADE,
232 'fluentform/rendering_field_html_' . $elementName,
233 'Use fluentform/rendering_field_html_' . $elementName . ' instead of fluentform_rendering_field_html_' . $elementName
234 );
235
236 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is escaped by the filter
237 echo apply_filters('fluentform/rendering_field_html_' . $elementName, $html, $data, $form);
238 }
239
240 public function renderMultiProduct($data, $form)
241 {
242 $elementName = $data['element'];
243
244 $data = apply_filters_deprecated(
245 'fluentform_rendering_field_data_' . $elementName,
246 [
247 $data,
248 $form
249 ],
250 FLUENTFORM_FRAMEWORK_UPGRADE,
251 'fluentform/rendering_field_data_' . $elementName,
252 'Use fluentform/rendering_field_data_' . $elementName . ' instead of fluentform_rendering_field_data_' . $elementName
253 );
254
255 $data = apply_filters('fluentform/rendering_field_data_' . $elementName, $data, $form);
256
257 // If all plans are expired and hidden, hide the entire field
258 $pricingPlans = ArrayHelper::get($data, 'settings.subscription_options', []);
259 $visiblePlans = array_filter($pricingPlans, function ($plan) {
260 return !$this->isPlanExpiredAndHidden($plan);
261 });
262
263 if (empty($visiblePlans)) {
264 return;
265 }
266
267 $currency = PaymentHelper::getFormCurrency($form->id);
268
269 $type = ArrayHelper::get($data, 'settings.selection_type', 'radio');
270 $data['attributes']['type'] = $type;
271
272 $data['settings']['container_class'] .= ' ff_subs_selections';
273
274 $isSmartUi = false;
275 if ($type == 'radio') {
276 $layoutClass = ArrayHelper::get($data, 'settings.layout_class');
277
278 if ($layoutClass == 'ff_list_buttons') {
279 $isSmartUi = true;
280 }
281
282 if ($layoutClass) {
283 $data['settings']['container_class'] .= ' ff_sub_smart_ui ' . $layoutClass;
284 }
285 }
286
287 $data['attributes']['class'] = trim(
288 'ff-el-form-check-input ' .
289 'ff-el-form-check-' . $data['attributes']['type'] . ' ' .
290 ArrayHelper::get($data, 'attributes.class')
291 );
292
293 $elMarkup = '';
294 $customAmountMarkup = '';
295 $firstTabIndex = \FluentForm\App\Helpers\Helper::getNextTabIndex();
296
297 if ($type === 'select') {
298 $selectAtts = [
299 'name' => $data['attributes']['name'],
300 'class' => 'ff-el-form-control ff_subscription_item ff_payment_item',
301 'data-subscription_item' => 'yes',
302 'data-name' => $data['attributes']['name'],
303 'id' => $this->getUniqueid(str_replace(['[', ']'], ['', ''], $data['attributes']['name'])),
304 'data-parent_input_name' => $data['attributes']['name'],
305 'data-parent_input_type' => $type,
306 ];
307
308 if ($firstTabIndex) {
309 $selectAtts['tabindex'] = $firstTabIndex;
310 }
311
312 $elMarkup .= '<select ' . $this->buildAttributes($selectAtts, $form) . '>';
313
314 $placeholder = ArrayHelper::get($data, 'settings.placeholder', __('--Select Plan--', 'fluentform'));
315
316 $elMarkup .= '<option value>' . $placeholder . '</option>';
317 }
318
319 $groupId = $this->makeElementId($data, $form);
320
321 foreach ($pricingPlans as $index => $pricingPlan) {
322 $pricingPlan['index'] = $index;
323
324 // Skip expired plans that are set to hide
325 if ($this->isPlanExpiredAndHidden($pricingPlan)) {
326 continue;
327 }
328
329 $isDefaultPlan = ArrayHelper::get($pricingPlan, 'is_default') === 'yes';
330
331 $isCustomAmount = ArrayHelper::get($pricingPlan, 'user_input') === 'yes';
332
333 if ($isCustomAmount) {
334 $pricingPlan['subscription_amount'] = ArrayHelper::get($pricingPlan, 'user_input_default_value');
335 }
336
337 $billingAttributes = $this->getPlanInputAttributes($pricingPlan);
338
339 if ($type === 'select') {
340 $optionAtts = array_merge($billingAttributes, [
341 'value' => $index,
342 'selected' => $isDefaultPlan
343 ]);
344 $optionAtts = $this->buildAttributes($optionAtts, $form);
345
346 $optionLabel = fluentform_sanitize_html($pricingPlan['name']);
347 $elMarkup .= '<option ' . $optionAtts . '>' . $optionLabel . '</option>';
348 } else {
349 $displayType = isset($data['settings']['display_type']) ? ' ff-el-form-check-' . $data['settings']['display_type'] : '';
350 $parentClass = "ff-el-form-check{$displayType}";
351 $atts = $data['attributes'];
352 $atts['checked'] = $isDefaultPlan;
353
354 if ($isDefaultPlan) {
355 $parentClass .= ' ff_item_selected';
356 }
357
358 if ($firstTabIndex) {
359 $atts['tabindex'] = $firstTabIndex;
360 $firstTabIndex = '-1';
361 }
362
363 $atts['class'] .= ' ff_subscription_item ff_payment_item';
364 $atts['value'] = $index;
365 $atts['data-group_id'] = $groupId;
366
367 $id = esc_attr($this->getUniqueid(str_replace(['[', ']'], ['', ''], $atts['name'])));
368 $atts = $this->buildAttributes(array_merge($billingAttributes, $atts), $form);
369
370 $safePlanName = fluentform_sanitize_html($pricingPlan['name']);
371 $labelHtml = "<span class='ff_plan_name'>{$safePlanName}</span>";
372
373 if ($isSmartUi) {
374 $paymentSummary = $this->getPaymentSummaryText($pricingPlan, $form->id, $currency);
375 $summaryHtml = '<div class="ff_sub_desc">' . $paymentSummary . '</div>';
376 $labelHtml = "<span class='ff_plan_holder'><span class='ff_plan_title'>{$safePlanName}</span>" . $summaryHtml . '</span>';
377 }
378
379 $elMarkup .= "<div class='" . esc_attr($parentClass) . "'>";
380 $elMarkup .= "<label class='ff-el-form-check-label' for='{$id}'><input {$atts} id='{$id}'>" . $labelHtml . '</label>';
381 $elMarkup .= "</div>";
382 }
383
384 if ($isCustomAmount) {
385 $customAmountMarkup = $this->makeCustomInputHtml($data, $pricingPlan, $type, $customAmountMarkup);
386 }
387
388 if (!$isSmartUi) {
389 $paymentSummary = $this->getPaymentSummaryText($pricingPlan, $form, $currency);
390 $customAmountMarkup .= $paymentSummary;
391 }
392 }
393
394 if ($type === 'select') {
395 $elMarkup .= '</select>';
396 }
397
398 if ($customAmountMarkup) {
399 $elMarkup .= $customAmountMarkup;
400 }
401
402 $html = $this->buildElementMarkup($elMarkup, $data, $form);
403
404 $html = apply_filters_deprecated(
405 'fluentform_rendering_field_html_' . $elementName,
406 [
407 $html,
408 $data,
409 $form
410 ],
411 FLUENTFORM_FRAMEWORK_UPGRADE,
412 'fluentform/rendering_field_html_' . $elementName,
413 'Use fluentform/rendering_field_html_' . $elementName . ' instead of fluentform_rendering_field_html_' . $elementName
414 );
415
416 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is escaped by the filter
417 echo apply_filters('fluentform/rendering_field_html_' . $elementName, $html, $data, $form);
418 }
419
420 private function getPaymentSummaryText($plan, $formId, $currency)
421 {
422 return PaymentHelper::getPaymentSummaryText($plan, $formId, $currency, true);
423 }
424
425 private function getPlanInputAttributes($plan)
426 {
427 $subscriptionAmount = $plan['subscription_amount'];
428 $currentBillableAmount = $subscriptionAmount;
429 $initialAmount = 0;
430
431 $signupFee = 0;
432 if ($this->hasSignupFee($plan)) {
433 $signupFee = $plan['signup_fee'];
434 }
435
436 $trialDays = false;
437 if ($this->hasTrial($plan)) {
438 $currentBillableAmount = 0;
439 $trialDays = ArrayHelper::get($plan, 'trial_days');
440 }
441
442 return [
443 'data-subscription_amount' => $subscriptionAmount ?: 0,
444 'data-billing_interval' => $plan['billing_interval'],
445 'data-price' => $currentBillableAmount ?: 0,
446 'data-payment_value' => $currentBillableAmount ?: 0,
447 'data-initial_amount' => $initialAmount ?: 0,
448 'data-signup_fee' => $signupFee,
449 'data-trial_days' => $trialDays,
450 'data-plan_name' => esc_attr($plan['name'])
451 ];
452 }
453
454 private function hasTrial($plan)
455 {
456 $hasTrial = ArrayHelper::get($plan, 'has_trial_days') == 'yes';
457 $trialDays = ArrayHelper::get($plan, 'trial_days');
458
459 return $hasTrial && $trialDays;
460 }
461
462 private function hasSignupFee($plan)
463 {
464 $hasSignup = ArrayHelper::get($plan, 'has_signup_fee') == 'yes';
465 $signUpFee = ArrayHelper::get($plan, 'signup_fee');
466
467 return $hasSignup && $signUpFee;
468 }
469
470 private function makeCustomInputHtml($field, $plan, $parentInputType, $markup)
471 {
472 $htmlID = ArrayHelper::get($field, 'attributes.name') . '_custom_' . $plan['index'];
473 $escapedHtmlID = esc_attr($htmlID);
474 $escapedPlanIndex = esc_attr($plan['index']);
475 $isDefault = ArrayHelper::get($plan, 'is_default') === 'yes';
476
477 $customAmountInputAttributes = $this->buildAttributes([
478 'name' => $htmlID,
479 'type' => 'number',
480 'value' => ArrayHelper::get($plan, 'user_input_default_value'),
481 'min' => $isDefault ? ArrayHelper::get($plan, 'user_input_min_value', 0) : '0',
482 'data-min' => ArrayHelper::get($plan, 'user_input_min_value', 0),
483 'step' => 'any',
484 'placeholder' => ArrayHelper::get($plan, 'user_input_label'),
485 'class' => 'ff-el-form-control ff-custom-user-input ' . ($isDefault ? 'is-default' : ''),
486 'id' => $htmlID,
487 'data-parent_input_name' => $field['attributes']['name'],
488 'data-parent_input_type' => $parentInputType,
489 'data-plan_index' => $plan['index'],
490 'data-plan_trial_days' => ArrayHelper::get($plan, 'trial_days', 0)
491 ]);
492
493 $class = $isDefault ? '' : 'hidden_field';
494 $style = $parentInputType === 'hidden' ? '' : "style='margin-top: 5px'";
495 $markup .= "<div class='ff-custom-user-input-wrapper ff-custom-user-input-wrapper-{$escapedPlanIndex} {$class}' {$style}>";
496
497 if ($parentInputType !== 'hidden') {
498 $customLabel = fluentform_sanitize_html(ArrayHelper::get($plan, 'user_input_label'));
499 $markup .= "<label class='ff-el-form-check-label' for='{$escapedHtmlID}'>{$customLabel}</label>";
500 }
501
502 $markup .= "<input {$customAmountInputAttributes}>";
503 $markup .= '</div>';
504
505 return $markup;
506 }
507
508 public function addWhiteListedFields($whiteListedFields, $formId)
509 {
510 $form = \FluentForm\App\Models\Form::find($formId);
511 if (!$form->has_payment) {
512 return $whiteListedFields;
513 }
514 $inputs = FormFieldsParser::getInputsByElementTypes($form, ['subscription_payment_component'], ['settings', 'attributes']);
515 $customInputNames = [];
516 foreach ($inputs as $subscriptionInput) {
517 $index = 0;
518 $subscriptionOptions = ArrayHelper::get($subscriptionInput, 'settings.subscription_options');
519 foreach ($subscriptionOptions as $plan) {
520 if (ArrayHelper::get($plan, 'user_input') === 'yes') {
521 $customInputNames[] = ArrayHelper::get($subscriptionInput, 'attributes.name') . '_custom_' . $index;
522 }
523 $index++;
524 }
525 }
526 return array_merge($whiteListedFields, $customInputNames);
527 }
528 }
529