PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.10
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.10
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.10, at app/Modules/Payments/Components/Subscription.php

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