PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / Renderer / CheckoutRenderer.php

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

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