PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / Renderer / CheckoutRenderer.php

CheckoutRenderer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.1, at app/Services/Renderer/CheckoutRenderer.php

1,035 lines 42.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Renderer;
4
5 use FluentCart\Api\PaymentMethods;
6 use FluentCart\Api\Resource\CustomerResource;
7 use FluentCart\Api\Resource\FrontendResource\CustomerAddressResource;
8 use FluentCart\Api\StoreSettings;
9 use FluentCart\App\App;
10 use FluentCart\App\Helpers\AddressHelper;
11 use FluentCart\App\Helpers\CartHelper;
12 use FluentCart\App\Models\Cart;
13 use FluentCart\App\Services\Localization\LocalizationManager;
14 use FluentCart\App\Services\URL;
15 use FluentCart\Framework\Support\Arr;
16
17 class CheckoutRenderer
18 {
19
20 private $cart;
21
22 private $requireShipping;
23
24 private $hasSubscription = false;
25
26 private $config = [];
27
28 private $billingAddress = [];
29
30 private $shippingAddress = [];
31
32 private $storeSettings;
33
34 public function __construct(Cart $cart, $config = [])
35 {
36 $this->cart = $cart;
37 $this->requireShipping = $cart->requireShipping();
38 $this->hasSubscription = $cart->hasSubscription();
39
40 $formData = Arr::get($cart->checkout_data, 'form_data', []);
41 $this->storeSettings = new StoreSettings();
42
43 $fallbackCountry = '';
44 $HTTP_CF_IP_COUNTRY = Arr::get(App::request()->server(), 'HTTP_CF_IPCOUNTRY');
45
46 if ($HTTP_CF_IP_COUNTRY) {
47 $fallbackCountry = $HTTP_CF_IP_COUNTRY;
48 }
49
50 $this->billingAddress = [
51 'full_name' => trim($this->cart->first_name . ' ' . $this->cart->last_name),
52 'country' => Arr::get($formData, 'billing_country', $fallbackCountry),
53 'address_1' => Arr::get($formData, 'billing_address_1', ''),
54 'address_2' => Arr::get($formData, 'billing_address_2', ''),
55 'city' => Arr::get($formData, 'billing_city', ''),
56 'state' => Arr::get($formData, 'billing_state', ''),
57 'postcode' => Arr::get($formData, 'billing_postcode', ''),
58 'company_name' => Arr::get($formData, 'billing_company_name', ''),
59 'phone' => Arr::get($formData, 'billing_phone', ''),
60 ];
61
62 if ($this->requireShipping) {
63 if (Arr::get($formData, 'ship_to_different', '') === 'yes') {
64 $shippingCountry = Arr::get($formData, 'shipping_country', $fallbackCountry);
65 $shippingValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements('shipping', 'physical'));
66 if (!Arr::has($shippingValidations, 'country')) {
67 $shippingCountry = Arr::get($formData, 'billing_country');
68 }
69
70 $this->shippingAddress = [
71 'full_name' => Arr::get($formData, 'shipping_full_name', ''),
72 'country' => $shippingCountry,
73 'address_1' => Arr::get($formData, 'shipping_address_1', ''),
74 'address_2' => Arr::get($formData, 'shipping_address_2', ''),
75 'city' => Arr::get($formData, 'shipping_city', ''),
76 'state' => Arr::get($formData, 'shipping_state', ''),
77 'postcode' => Arr::get($formData, 'shipping_postcode', ''),
78 'company_name' => Arr::get($formData, 'shipping_company_name', ''),
79 'phone' => Arr::get($formData, 'shipping_phone', ''),
80 ];
81 } else {
82 $this->shippingAddress = $this->billingAddress;
83 }
84 }
85
86 $this->config = $config;
87 }
88
89 public function render($config = [])
90 {
91 if ($config) {
92 $this->config = wp_parse_args($config, $this->config);
93 }
94
95 $this->wrapperStart();
96 $this->renderNotices();
97
98 $this->renderCheckoutForm();
99
100 $this->wrapperEnd();
101 }
102
103 public function getFragment($fragmentName)
104 {
105 $maps = [
106 'shipping_methods' => 'renderShippingOptions',
107 'payment_methods' => 'renderPaymentMethods',
108 'cart_summary_fragment' => 'renderSummaryFragment'
109 ];
110
111 if (isset($maps[$fragmentName])) {
112 ob_start();
113 $this->{$maps[$fragmentName]}();
114 return ob_get_clean();
115 }
116 return '';
117
118 }
119
120 public function wrapperStart()
121 {
122 $classNames = [
123 'fluent-cart-checkout-page',
124 'fct-checkout',
125 'fct-checkout-type-' . $this->cart->cart_group
126 ];
127 $configClass = Arr::get($this->config, 'wrapper_class', '');
128
129 if ($configClass) {
130 $classNames[] = $configClass;
131 }
132
133 $classNames = apply_filters('fluent_cart/checkout_page_css_classes', $classNames, [
134 'cart' => $this->cart
135 ]);
136
137 $classNames = array_filter(array_unique($classNames));
138
139 $atts = [
140 'class' => implode(' ', $classNames),
141 'data-fluent-cart-checkout-page' => '',
142 ];
143
144 do_action('fluent_cart/before_checkout_page_start', [
145 'cart' => $this->cart
146 ]);
147 ?>
148 <div <?php RenderHelper::renderAtts($atts); ?> role="main"
149 aria-label="<?php esc_attr_e('Checkout Page', 'fluent-cart'); ?>">
150 <?php
151 $hookData = [
152 'cart' => $this->cart
153 ];
154 do_action_deprecated('fluent_cart/afrer_checkout_page_start', [$hookData], '1.3.16', 'fluent_cart/after_checkout_page_start', 'Use fluent_cart/after_checkout_page_start instead of fluent_cart/afrer_checkout_page_start. It will be removed in v1.4.3.');
155 do_action('fluent_cart/after_checkout_page_start', $hookData);
156 }
157
158 public function renderNotices()
159 {
160 $notices = Arr::get($this->cart->checkout_data, '__cart_notices', []);
161 $hookedNotices = apply_filters('fluent_cart/checkout_page_notices', [], [
162 'cart' => $this->cart
163 ]);
164 if (!$notices && !$hookedNotices) {
165 return;
166 }
167 ?>
168 <div class="fct-cart-notices" role="status" aria-live="polite">
169 <?php foreach ($notices as $notice):
170 if (empty($notice['content'])) {
171 continue;
172 } ?>
173 <div class="fct-alert">
174 <?php echo wp_kses_post($notice['content']); ?>
175 </div>
176 <?php endforeach; ?>
177 <?php foreach ($hookedNotices as $notice):
178 if (empty($notice['content'])) {
179 continue;
180 } ?>
181 <div class="fct-alert">
182 <?php echo wp_kses_post($notice['content']); ?>
183 </div>
184 <?php endforeach; ?>
185 </div>
186 <?php
187 }
188
189 public function renderCheckoutForm()
190 {
191 global $wp;
192 $current_url = home_url(add_query_arg([], $wp->request));
193 $current_url = add_query_arg(App::request()->all(), $current_url);
194
195 $formAttributes = [
196 'method' => 'POST',
197 'data-fluent-cart-checkout-page-checkout-form' => '',
198 'class' => 'fct_checkout fluent-cart-checkout-page-checkout-form',
199 'action' => $current_url,
200 'enctype' => 'multipart/form-data',
201 ];
202 do_action('fluent_cart/before_checkout_form', ['cart' => $this->cart]);
203 ?>
204 <form <?php RenderHelper::renderAtts($formAttributes); ?>
205 aria-label="<?php esc_attr_e('Checkout Form', 'fluent-cart'); ?>">
206 <?php do_action('fluent_cart/checkout_form_opening', ['cart' => $this->cart]); ?>
207 <div class="fct_checkout_inner">
208 <div class="fct_checkout_form">
209 <div class="fct_checkout_form_items">
210 <?php $this->renderNameFields(); ?>
211
212 <?php $this->renderCreateAccountField(); ?>
213
214 <?php do_action('fluent_cart/before_billing_fields', ['cart' => $this->cart]); ?>
215
216 <?php $this->renderAddressFields(); ?>
217 <div class="fct_checkout_shipping_methods <?php echo $this->requireShipping ? '' : 'is-hidden' ?>">
218 <?php $this->renderShippingOptions(); ?>
219 </div>
220
221 <?php $this->agreeTerms(); ?>
222
223 <?php do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]); ?>
224
225 <div class="fct_checkout_payment_methods " data-fluent-cart-checkout-payment-methods>
226 <?php $this->renderPaymentMethods(); ?>
227 </div>
228
229 <?php do_action('fluent_cart/after_payment_methods', ['cart' => $this->cart]); ?>
230
231 <?php $this->renderCheckoutButton(); ?>
232
233 <?php do_action('fluent_cart/after_checkout_button', ['cart' => $this->cart]); ?>
234
235 </div>
236 </div>
237 <div class="fct_checkout_summary">
238 <div class="fct_summary active" data-fluent-cart-checkout-page-checkout-form-order-summary
239 aria-labelledby="order-summary-heading">
240 <span id="order-summary-heading" class="sr-only">
241 <?php esc_html_e('Order Summary', 'fluent-cart'); ?>
242 </span>
243
244 <?php (new CartSummaryRender($this->cart))->render(); ?>
245 <?php $this->renderOrderNoteField(); ?>
246 <?php do_action('fluent_cart/after_order_notes', ['cart' => $this->cart]); ?>
247 </div>
248 </div>
249 </div>
250 </form>
251 <?php
252 do_action('fluent_cart/after_checkout_form', ['cart' => $this->cart]);
253 }
254
255 public function wrapperEnd()
256 {
257 do_action('fluent_cart/before_checkout_page_close', [
258 'cart' => $this->cart
259 ]);
260 ?>
261 </div>
262 <?php
263 do_action('fluent_cart/after_checkout_page', [
264 'cart' => $this->cart
265 ]);
266 }
267
268 public function renderCreateAccountField($atts = [])
269 {
270 //check store settings also
271 $attr_title = Arr::get($atts, 'title');
272 $extraClass = Arr::get($atts, 'wrapper_atts') ? '' : 'fct-has-default-font-size';
273
274 ?>
275 <?php if (!is_user_logged_in() && $this->storeSettings->get('user_account_creation_mode') === 'user_choice'): ?>
276 <div class="fct_allow_create_account_wrapper <?php echo esc_attr($extraClass); ?>">
277 <?php
278 $formRender = new FormFieldRenderer();
279 $label = __('Create an account?', 'fluent-cart');
280 if (!empty($attr_title)) {
281 $label = $attr_title;
282 }
283 $formRender->renderField([
284 'type' => 'checkbox',
285 'id' => 'allow_create_account',
286 'name' => 'allow_create_account',
287 'checkbox_value' => 'yes',
288 'label' => $label,
289 'value' => Arr::get($this->cart->checkout_data, 'form_data.allow_create_account', ''),
290 'wrapper_class' => 'fct_create_account_wrapper',
291 ]);
292 ?>
293 </div>
294 <?php endif;?>
295
296 <?php
297 }
298
299 public function renderNameFields()
300 {
301 $schema = CheckoutFieldsSchema::getNameEmailFieldsSchema($this->cart);
302 if (!$schema) {
303 return;
304 }
305 (new FormFieldRenderer())->renderSection($schema);
306 }
307
308 public function renderB2BToggle()
309 {
310 if (!CheckoutFieldsSchema::hasAnyBusinessFields()) {
311 return;
312 }
313
314 if (CheckoutFieldsSchema::isB2BOnlyMode()) {
315 $this->renderBusinessDetailsSection(true);
316 return;
317 }
318
319 $isB2B = Arr::get($this->cart->checkout_data, 'form_data.is_business', 'no') === 'yes';
320
321 $hasBusinessSection = CheckoutFieldsSchema::isCompanyNameEnabled()
322 || CheckoutFieldsSchema::isLegalRegistrationIdEnabled()
323 || CheckoutFieldsSchema::isVatNumberEnabled();
324
325 $extraAtts = [
326 'data-fluent-cart-b2b-toggle' => 'yes',
327 'aria-expanded' => $isB2B ? 'true' : 'false',
328 ];
329 if ($hasBusinessSection) {
330 $extraAtts['aria-controls'] = 'fct_b2b_business_section';
331 }
332 ?>
333 <div class="fct_b2b_toggle_wrapper fct-has-default-font-size">
334 <?php
335 (new FormFieldRenderer())->renderField([
336 'type' => 'checkbox',
337 'id' => 'is_business',
338 'name' => 'is_business',
339 'checkbox_value' => 'yes',
340 'label' => __('I am purchasing as a business', 'fluent-cart'),
341 'value' => $isB2B ? 'yes' : '',
342 'extra_atts' => $extraAtts,
343 ]);
344 ?>
345 </div>
346 <?php
347 $this->renderBusinessDetailsSection($isB2B);
348 }
349
350 public function renderBusinessDetailsSection($isVisible)
351 {
352 $hasCompany = CheckoutFieldsSchema::isCompanyNameEnabled();
353 $hasLegalReg = CheckoutFieldsSchema::isLegalRegistrationIdEnabled();
354 $hasVat = CheckoutFieldsSchema::isVatNumberEnabled();
355
356 if (!$hasCompany && !$hasLegalReg && !$hasVat) {
357 return;
358 }
359
360 $formData = Arr::get($this->cart->checkout_data, 'form_data', []);
361 $isCompanyRequired = CheckoutFieldsSchema::isCompanyNameRequired();
362 $isLegalRequired = CheckoutFieldsSchema::isLegalRegistrationIdRequired();
363 $style = $isVisible ? '' : 'display:none';
364 ?>
365 <div id="fct_b2b_business_section"
366 data-fluent-cart-b2b-section
367 class="fct_checkout_form_section fct_b2b_business_section"
368 style="<?php echo esc_attr($style); ?>"
369 role="group"
370 aria-label="<?php esc_attr_e('Business Details', 'fluent-cart'); ?>">
371 <div class="fct_form_section_body">
372 <div class="fct_checkout_input_group">
373 <?php if ($hasCompany): ?>
374 <?php
375 $companyLabel = __('Company Name', 'fluent-cart');
376 $companyValue = Arr::get($formData, 'billing_company_name', '');
377 ?>
378 <div data-fluent-cart-checkout-page-form-input-wrapper
379 class="fct_input_wrapper"
380 id="billing_company_name_wrapper">
381 <label for="billing_company_name" class="sr-only">
382 <?php echo esc_html($companyLabel); ?>
383 </label>
384 <input type="text"
385 name="billing_company_name"
386 id="billing_company_name"
387 autocomplete="organization"
388 placeholder="<?php echo esc_attr($companyLabel . ($isCompanyRequired ? ' *' : '')); ?>"
389 value="<?php echo esc_attr($companyValue); ?>"
390 aria-label="<?php echo esc_attr($companyLabel); ?>"
391 <?php if ($isCompanyRequired): ?> data-b2b-required="yes"<?php endif; ?>
392 <?php if ($isCompanyRequired && $isVisible): ?> required aria-required="true"<?php endif; ?>
393 />
394 </div>
395 <?php endif; ?>
396
397 <?php if ($hasLegalReg): ?>
398 <?php
399 $legalLabel = __('Legal Registration ID', 'fluent-cart');
400 $legalValue = Arr::get($formData, 'billing_legal_registration_id', '');
401 ?>
402 <div data-fluent-cart-checkout-page-form-input-wrapper
403 class="fct_input_wrapper"
404 id="billing_legal_registration_id_wrapper">
405 <label for="billing_legal_registration_id" class="sr-only">
406 <?php echo esc_html($legalLabel); ?>
407 </label>
408 <input type="text"
409 name="billing_legal_registration_id"
410 id="billing_legal_registration_id"
411 autocomplete="off"
412 placeholder="<?php echo esc_attr($legalLabel . ($isLegalRequired ? ' *' : '')); ?>"
413 value="<?php echo esc_attr($legalValue); ?>"
414 aria-label="<?php echo esc_attr($legalLabel); ?>"
415 <?php if ($isLegalRequired): ?> data-b2b-required="yes"<?php endif; ?>
416 <?php if ($isLegalRequired && $isVisible): ?> required aria-required="true"<?php endif; ?>
417 />
418 </div>
419 <?php endif; ?>
420 <?php do_action('fluent_cart/checkout/b2b_extra_fields', ['cart' => $this->cart]); ?>
421 </div>
422 </div>
423 </div>
424 <?php
425 }
426
427 public function validateAddressField($config, $fields)
428 {
429
430 $type = Arr::get($config, 'type', 'billing'); // billing or shipping
431
432 $customer = CustomerResource::getCurrentCustomer();
433 if ($customer) {
434 $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements(
435 $type,
436 Arr::get($config, 'product_type'),
437 Arr::get($config, 'with_shipping')
438 );
439 $allowedAddresses = AddressHelper::getCustomerValidatedAddresses($config, $customer);
440
441 if (!empty($allowedAddresses)) {
442 $primaryAddress = AddressHelper::getPrimaryAddress(
443 $allowedAddresses,
444 $config,
445 $customer,
446 $type
447 );
448
449
450 $addressLabel = $type === 'billing' ?
451 __('Billing Address', 'fluent-cart') :
452 __('Shipping Address', 'fluent-cart');
453
454 $countries = LocalizationManager::getInstance()->countries();
455
456 return [
457 'address_select' => [
458 'type' => 'address_select',
459 'address_type' => $type,
460 'options' => $allowedAddresses,
461 'title' => $addressLabel,
462 'label' => '',
463 'countries' => $countries,
464 'primary_address' => $primaryAddress,
465 'value' => Arr::get($primaryAddress, 'id'),
466 'requirements_fields' => $requirementsFields
467 ]
468 ];
469 }
470
471 return $fields;
472 }
473
474 return $fields;
475 }
476
477 public function renderAddressFields()
478 {
479 $requireShipping = $this->requireShipping;
480 // $formData = Arr::get($this->cart->checkout_data, 'form_data', []);
481
482
483 // $billingAddress = $this->billingAddress;
484
485
486 // $billingAddress['type'] = 'billing';
487 // $billingAddress['product_type'] = $requireShipping ? 'physical' : 'digital';
488 // $billingAddress['with_shipping'] = $requireShipping && Arr::get($formData, 'ship_to_different', '') !== 'yes';
489 // $billingAddress['billing_address_id'] = Arr::get($formData, 'billing_address_id', '');
490
491 // $billingFields = CheckoutFieldsSchema::getAddressBaseFields($billingAddress);
492
493 // $billingFields = $this->validateAddressField($billingAddress, $billingFields);
494
495
496 // foreach ($billingFields as &$field) {
497 // if(empty($field['wrapper_atts'])) {
498 // $field['wrapper_atts'] = [];
499 // }
500 // $field['wrapper_atts']['data-fluent-cart-checkout-page-form-input-wrapper'] = '';
501 // }
502
503 // $billingFields = $this->maybeRearrangeAddressFields($billingFields);
504
505
506 // $billingFields = apply_filters('fluent_cart/checkout_renderer/billing_fields', $billingFields, [
507 // 'checkout_renderer' => $this,
508 // 'cart' => $this->cart
509 // ]);
510
511
512 // $formRender = new FormFieldRenderer();
513
514 echo '<div class="fct_checkout_billing_and_shipping">';
515
516 $this->renderBillingAddressFields();
517
518 $this->renderB2BToggle();
519
520 if (!$requireShipping) {
521 echo '</div>';
522 return;
523 }
524
525 $this->renderShipToDifferentField();
526 do_action('fluent_cart/after_billing_fields_section', ['cart' => $this->cart]);
527
528 $this->renderShippingAddressFields();
529 echo '</div>';
530 }
531
532 public function renderBillingAddressFields($section_title = '')
533 {
534
535 $formData = Arr::get($this->cart->checkout_data, 'form_data', []);
536 $requireShipping = $this->requireShipping;
537 $billingAddress = $this->billingAddress;
538
539 $billingAddress['type'] = 'billing';
540 $billingAddress['product_type'] = $requireShipping ? 'physical' : 'digital';
541 $billingAddress['with_shipping'] = $requireShipping && Arr::get($formData, 'ship_to_different', '') !== 'yes';
542 $billingAddress['billing_address_id'] = Arr::get($formData, 'billing_address_id', '');
543 $billingAddress['order_id'] = $this->cart->order_id ?? null;
544
545 $billingFields = CheckoutFieldsSchema::getAddressBaseFields($billingAddress);
546
547 $billingFields = $this->validateAddressField($billingAddress, $billingFields);
548
549 foreach ($billingFields as &$field) {
550 if (empty($field['wrapper_atts'])) {
551 $field['wrapper_atts'] = [];
552 }
553 $field['wrapper_atts']['data-fluent-cart-checkout-page-form-input-wrapper'] = '';
554 }
555
556 $billingFields = $this->maybeRearrangeAddressFields($billingFields);
557
558
559 $billingFields = apply_filters('fluent_cart/checkout_renderer/billing_fields', $billingFields, [
560 'checkout_renderer' => $this,
561 'cart' => $this->cart
562 ]);
563
564 do_action('fluent_cart/before_billing_fields_section', ['cart' => $this->cart]);
565
566 $formRender = new FormFieldRenderer();
567 $title = __('Billing Address', 'fluent-cart');
568 if (!empty($section_title)) {
569 $title = $section_title;
570 }
571
572 $formRender->renderSection([
573 'id' => 'billing_address_section_section',
574 'type' => 'section',
575 'heading' => $title,
576 'fields' => $billingFields,
577 'wrapper_atts' => [
578 'data-fluent-cart-checkout-page-form-section' => '',
579 'role' => 'region',
580 'aria-label' => __('Billing Address', 'fluent-cart')
581 ]
582 ]);
583 }
584
585 public function renderShipToDifferentField($atts = [])
586 {
587 $formRender = new FormFieldRenderer();
588 $attr_title = Arr::get($atts, 'title');
589 $extraClass = Arr::get($atts, 'wrapper_atts') ? '' : 'fct-has-default-font-size';
590 $title = __('Ship to a different address?', 'fluent-cart');
591 if (!empty($attr_title)) {
592 $title = $attr_title;
593 }
594
595 ?>
596
597 <div class="fct_ship_to_different_wrapper <?php echo esc_attr($extraClass); ?>">
598 <?php
599 $formRender->renderField([
600 'type' => 'checkbox',
601 'id' => 'ship_to_different',
602 'name' => 'ship_to_different',
603 'checkbox_value' => 'yes',
604 'label' => $title,
605 'value' => Arr::get($this->cart->checkout_data, 'form_data.ship_to_different', ''),
606 'extra_atts' => [
607 'data-fluent-cart-ship-to-different-address' => 'yes',
608 'aria-controls' => 'shipping_address_section_section',
609 ],
610 ]);
611 ?>
612 </div>
613
614 <?php
615 }
616
617 public function renderShippingAddressFields($section_title = '')
618 {
619 $formData = Arr::get($this->cart->checkout_data, 'form_data', []);
620
621 $formRender = new FormFieldRenderer();
622
623 $shippingAddress = $this->shippingAddress;
624 $shippingAddress['type'] = 'shipping';
625 $shippingAddress['product_type'] = 'physical';
626 $shippingAddress['shipping_address_id'] = Arr::get($formData, 'shipping_address_id', '');
627
628 $shippingFields = CheckoutFieldsSchema::getAddressBaseFields($shippingAddress);
629
630 $shippingFields = $this->validateAddressField($shippingAddress, $shippingFields);
631
632 $shippingFields = apply_filters('fluent_cart/checkout_renderer/shipping_fields', $shippingFields, [
633 'checkout_renderer' => $this,
634 'cart' => $this->cart
635 ]);
636
637 foreach ($shippingFields as &$field) {
638 if (empty($field['wrapper_atts'])) {
639 $field['wrapper_atts'] = [];
640 }
641 $field['wrapper_atts']['data-fluent-cart-checkout-page-form-input-wrapper'] = '';
642 }
643
644 $shippingFields = $this->maybeRearrangeAddressFields($shippingFields);
645
646 $title = __('Shipping Address', 'fluent-cart');
647 if (!empty($section_title)) {
648 $title = $section_title;
649 }
650
651 do_action('fluent_cart/before_shipping_fields_section', ['cart' => $this->cart]);
652 $formRender->renderSection([
653 'id' => 'shipping_address_section_section',
654 'type' => 'section',
655 'heading' => $title,
656 'fields' => $shippingFields,
657 'wrapper_atts' => [
658 'data-fluent-cart-checkout-page-shipping-fields' => '',
659 'style' => Arr::get($this->cart->checkout_data, 'form_data.ship_to_different', '') === 'yes' ? '' : 'display:none',
660 'role' => 'region',
661 'aria-label' => __('Shipping Address', 'fluent-cart')
662 ]
663 ]);
664 do_action('fluent_cart/after_shipping_fields_section', ['cart' => $this->cart]);
665 }
666
667 public function renderOrderNoteField($attr_title = '')
668 {
669 if (
670 !$this->requireShipping &&
671 apply_filters('fluent_cart/disable_order_notes_for_digital_products', true, [
672 'cart' => $this->cart
673 ])
674 ) {
675 return;
676 }
677
678 $noteTitle = __('Leave a Note', 'fluent-cart');
679 if (!empty($attr_title)) {
680 $noteTitle = $attr_title;
681 }
682 $fieldId = 'order_notes';
683
684 (new FormFieldRenderer())->renderField([
685 'type' => 'textarea',
686 'id' => $fieldId,
687 'name' => 'order_notes',
688 'aria-label' => __('Order Notes', 'fluent-cart'),
689 'placeholder' => __('Notes about your order, e.g. Leave it at my doorstep.', 'fluent-cart'),
690 'extra_atts' => [
691 'rows' => 4
692 ],
693 'wrapper_atts' => [
694 'data-fct-item-toggle' => '',
695 'class' => 'fct-toggle-field fct_order_note'
696 ],
697 'before_callback' => function ($field) use ($fieldId, $noteTitle) {
698 $toggleId = 'order_notes_toggle';
699 $wrapperId = 'order_notes_wrapper';
700 ?>
701
702 <button type="button" data-fct-item-toggle-control id="<?php echo esc_attr($toggleId); ?>"
703 class="fct-toggle-control fct_order_note_toggle" aria-expanded="false"
704 aria-controls="<?php echo esc_attr($wrapperId); ?>">
705 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none">
706 <path d="M15.6 12.0001L10.2 17.4001V6.6001L15.6 12.0001Z" fill="currentColor" />
707 </svg>
708 <?php echo esc_html($noteTitle); ?>
709 </button>
710
711 <div id="<?php echo esc_attr($wrapperId); ?>" class="fct_toggle-wrapper fct_order_note_wrapper" aria-hidden="true">
712 <?php
713 },
714 'after_callback' => function ($field) {
715 echo '</div>';
716 }
717 ]);
718
719 do_action('fluent_cart/after_order_notes_field', ['cart' => $this->cart]);
720 }
721
722 public function renderShippingOptions()
723 {
724 if (!$this->requireShipping) {
725 return;
726 }
727
728 $countryCode = $this->shippingAddress['country'] ?: $this->billingAddress['country'];
729 $stateCode = $this->shippingAddress['state'] ?: $this->billingAddress['state'];
730 $billingValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements('billing', 'physical'));
731
732 if (!isset($billingValidations['country'])) {
733 $countryCode = (new StoreSettings())->get('store_country');
734 }
735
736 $availableShippingMethods = AddressHelper::getShippingMethods($countryCode, $stateCode);
737
738
739 $selectedId = Arr::get($this->cart->checkout_data, 'shipping_data.shipping_method_id', '');
740
741 $selectedId = CartHelper::resolveAutoSelectShippingMethod($this->cart, $availableShippingMethods ?: [], $selectedId);
742
743 if (!$availableShippingMethods || is_wp_error($availableShippingMethods)) {
744 (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render();
745 } else {
746 foreach ($availableShippingMethods as $method) {
747 $method->charge_amount = CartHelper::calculateShippingMethodCharge($method, $this->cart->cart_data);
748 }
749
750 (new ShippingMethodsRender($availableShippingMethods, $selectedId))->render();
751 }
752 }
753
754 /**
755 * Get available shipping methods considering cart's shipping classes.
756 * This is used by the frontend to display profile-aware methods.
757 */
758 public function getProfileAwareShippingMethods($countryCode, $stateCode)
759 {
760 // Get general methods (backward compatible)
761 $methods = AddressHelper::getShippingMethods($countryCode, $stateCode);
762
763 if (is_wp_error($methods) || empty($methods)) {
764 return $methods;
765 }
766
767 return $methods;
768 }
769
770 public function renderPaymentMethods($atts = [])
771 {
772 if ($this->cart->getEstimatedTotal() <= 0) {
773 if (!$this->cart->hasSubscription() || $this->cart->getEstimatedRecurringTotal() <= 0) {
774 return '';
775 }
776 }
777
778 $selectedPaymentMethod = Arr::get($this->cart->checkout_data, 'form_data._fct_pay_method', '');
779 $activePaymentMethods = PaymentMethods::getActiveMethodInstance($this->cart);
780 $hadActiveMethods = !empty($activePaymentMethods);
781
782 $activePaymentMethods = apply_filters('fluent_cart/checkout_active_payment_methods', $activePaymentMethods, [
783 'cart' => $this->cart
784 ]);
785
786 if (!$selectedPaymentMethod && !empty($activePaymentMethods)) {
787 // reset() not [0] — the filter above may return a non-zero-indexed array.
788 $firstMethod = reset($activePaymentMethods);
789 $selectedPaymentMethod = $firstMethod ? $firstMethod->getMeta('route') : '';
790 }
791
792 $checkoutMethodStyle = $this->storeSettings->get('checkout_method_style', 'logo');
793
794 ?>
795 <div id="fluent_payment_methods" class="fluent_payment_methods">
796 <div class="fct_checkout_form_section" aria-labelledby="payment_methods_label" role="radiogroup">
797 <div class="fct_form_section_header">
798 <h4 id="payment_methods_label" class="fct_form_section_header_label">
799 <?php esc_html_e('Payment', 'fluent-cart'); ?>
800 </h4>
801 </div>
802 <div class="fct_form_section_body">
803 <div
804 class="fct_payment_methods_list fct_payment_method_mode_<?php echo esc_attr($checkoutMethodStyle); ?>">
805 <?php if (!empty($activePaymentMethods)): ?>
806 <?php foreach ($activePaymentMethods as $method): ?>
807 <?php
808 $isSelected = ($selectedPaymentMethod === $method->getMeta('route'));
809
810 $this->renderPaymentMethod($method, [
811 'selected_id' => $selectedPaymentMethod,
812 'style' => $checkoutMethodStyle,
813 'aria_checked' => $isSelected ? 'true' : 'false',
814 'role' => 'radio'
815 ]);
816 ?>
817 <?php endforeach; ?>
818 <?php else: ?>
819 <?php
820 if ($hadActiveMethods) {
821 $emptyText = esc_html__('None of the available payment methods can process this order. Please contact the store.', 'fluent-cart');
822 } else {
823 $emptyText = esc_html__('No Payment method is activated for this site yet.', 'fluent-cart');
824 }
825 if (current_user_can('manage_options')) {
826 $emptyText .= '<a href="' . esc_url(URL::getDashboardUrl('settings/payments')) . '" target="_blank">' . esc_html__('Activate from settings.', 'fluent-cart') . '</a>';
827 }
828 echo '<div class="fct-empty-state">' . wp_kses_post($emptyText) . '</div>';
829 ?>
830 <?php endif; ?>
831 </div>
832 </div>
833 </div>
834 </div>
835 <?php
836 }
837
838 public function renderCheckoutButton($atts = '')
839 {
840
841 $placeOrderButtonText = apply_filters('fluent_cart/checkout_page_order_button_text', __('Place order', 'fluent-cart'));
842 $attributes = [
843 'type' => 'submit',
844 'class' => 'fct_place_order_btn large',
845 'id' => 'fluent_cart_order_btn',
846 'data-fluent-cart-checkout-page-checkout-button' => '',
847 'data-value' => $placeOrderButtonText,
848 'disabled' => ''
849 ];
850
851 // Parse $atts using WordPress shortcode_atts or wp_parse_args
852 if (!empty($atts)) {
853 // Extract attributes from string
854 $parsed = shortcode_parse_atts($atts);
855
856 if (isset($parsed['class'])) {
857 $attributes['class'] .= ' ' . $parsed['class'];
858 unset($parsed['class']);
859 }
860
861 $attributes = array_merge($attributes, $parsed);
862 }
863
864 ?>
865 <div class="fct_place_order_btn_wrap">
866 <button <?php RenderHelper::renderAtts($attributes); ?>>
867 <?php echo esc_html($placeOrderButtonText); ?>
868 </button>
869 </div>
870 <?php
871 }
872
873 protected function renderPaymentMethod($method, $config = [])
874 {
875 $route = $method->getMeta('route');
876 $methodTitle = $method->getMeta('title');
877 $methodStyle = Arr::get($config, 'style', 'logo');
878
879 $inputAttributes = array_filter([
880 'class' => 'form-radio-input',
881 'type' => 'radio',
882 'name' => '_fct_pay_method',
883 'id' => 'fluent_cart_payment_method_' . $route,
884 'value' => $route,
885 'required' => true,
886 'checked' => $route === Arr::get($config, 'selected_id', '') ? 'true' : '',
887 'role' => Arr::get($config, 'role', 'radio'),
888 'aria-checked' => Arr::get($config, 'aria_checked', 'false'),
889 ]);
890
891 $wrapperClass = $methodStyle === 'logo' ? 'fct_payment_method_logo' : 'fct_payment_method';
892
893 $wrapperAttributes = [
894 'class' => $wrapperClass . ' ' . 'fct_payment_method_wrapper fct_payment_method_' . $route,
895 'tabindex' => '0',
896 'role' => 'presentation'
897 ];
898
899 ?>
900 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
901 <span class="fct-payment-method-loader">
902 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
903 <circle cx="12" cy="12" r="10" opacity="0.2" fill="none" stroke="currentColor" stroke-miterlimit="10"
904 stroke-width="2.5"></circle>
905
906 <path d="m12,2c5.52,0,10,4.48,10,10" fill="none" stroke="currentColor" stroke-linecap="round"
907 stroke-miterlimit="10" stroke-width="2.5">
908 <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="0.5s"
909 from="0 12 12" to="360 12 12" repeatCount="indefinite"></animateTransform>
910 </path>
911 </svg>
912 </span>
913
914 <input <?php RenderHelper::renderAtts($inputAttributes); ?> />
915 <label for="<?php echo esc_attr('fluent_cart_payment_method_' . $route); ?>">
916 <?php
917 if ($methodStyle === 'logo') {
918 $method->prepare('logo', $this->hasSubscription);
919 } else {
920 $method->prepare('radio', $this->hasSubscription);
921 }
922 ?>
923
924 <?php echo esc_html($methodTitle); ?>
925 </label>
926 <?php if ($method->getMeta('instructions')): ?>
927 <div class="fct_payment_method_instructions" style="display: none;">
928 <?php echo wp_kses_post($method->getMeta('instructions')); ?>
929 </div>
930 <?php endif; ?>
931 <div class="fluent-cart-checkout_embed_payment_wrapper">
932 <?php
933 $pmContext = [
934 'route' => $route,
935 'method_title' => $methodTitle,
936 'method_style' => $methodStyle,
937 ];
938 $paymentMethodClass = apply_filters_deprecated('fluent_cart_payment_method_list_class', ['', $pmContext], '1.3.16', 'fluent_cart/payment_method_list_class', 'Use fluent_cart/payment_method_list_class instead of fluent_cart_payment_method_list_class.');
939 $paymentMethodClass = apply_filters('fluent_cart/payment_method_list_class', $paymentMethodClass, $pmContext);
940
941 ?>
942 <div class="<?php echo "fluent-cart-checkout_embed_payment_container fluent-cart-checkout_embed_payment_container_" . esc_attr($route . ' ' . $paymentMethodClass); ?>"
943 aria-hidden="true">
944 <?php do_action(
945 'fluent_cart/checkout_embed_payment_method_content',
946 [
947 'method' => $method,
948 'cart' => $this->cart,
949 'route' => $route
950 ]
951 ); ?>
952 </div>
953 </div>
954 </div>
955 <?php
956 }
957
958 private function maybeRearrangeAddressFields($fields)
959 {
960 if (isset($fields['city']) && isset($fields['postcode'])) {
961 $cityField = $fields['city'];
962 $postcodeField = $fields['postcode'];
963 unset($fields['city'], $fields['postcode']);
964 $fields['city_postal'] = [
965 'type' => 'sub_section',
966 'wrapper_class' => 'fct_2_columns fct_checkout_city_postcode',
967 'fields' => [
968 'city' => $cityField,
969 'postcode' => $postcodeField
970 ],
971 ];
972 }
973
974 if (isset($fields['phone'])) {
975 $phoneField = $fields['phone'];
976 unset($fields['phone']);
977 $fields['phone'] = $phoneField;
978 }
979
980 if (isset($fields['company_name'])) {
981 $companyField = $fields['company_name'];
982 unset($fields['company_name']);
983 $fields['company_name'] = $companyField;
984 }
985
986 return $fields;
987 }
988
989
990 public function agreeTerms($atts = [])
991 {
992 if (!CheckoutFieldsSchema::isTermsVisible()) {
993 return;
994 }
995
996 $termsText = CheckoutFieldsSchema::getTermsText();
997 $sectionId = 'agree_terms_section';
998 $extraClass = Arr::get($atts, 'wrapper_atts') ? '' : 'fct-has-default-font-size';
999 $title = Arr::get($atts, 'title');
1000
1001 ?>
1002 <div class="fct_checkout_form_section <?php echo esc_attr($extraClass); ?>" role="group" aria-labelledby="agree_terms_label" data-fct-checkout-form-section>
1003 <div class="fct_form_section_body">
1004 <div class="fct_checkout_agree_terms">
1005 <div>
1006 <label for="agree_terms" class="fct_input_label fct_input_label_checkbox">
1007 <input data-fluent-cart-agree-terms="yes" type="checkbox" class="fct-input fct-input-checkbox"
1008 id="agree_terms" name="agree_terms" value="yes" required aria-required="true"
1009 aria-label="<?php echo esc_attr($termsText); ?>">
1010 <?php
1011 if (!empty($title)) {
1012 echo esc_html($title);
1013 } else {
1014 echo wp_kses_post($termsText);
1015 } ?>
1016 </label>
1017 <span
1018 id="<?php echo esc_attr($sectionId); ?>"
1019 data-fluent-cart-checkout-page-form-error=""
1020 class="fct_form_error fct_error_<?php echo esc_attr($sectionId); ?>"
1021 role="alert"
1022 aria-live="polite"
1023 ></span>
1024 </div>
1025 </div>
1026 </div>
1027 </div>
1028 <?php }
1029
1030 private function renderSummaryFragment()
1031 {
1032 (new CartSummaryRender($this->cart))->render(false);
1033 }
1034 }
1035