PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.0
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.4.0, at app/Services/Renderer/CheckoutRenderer.php

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