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

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

1,028 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_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
781 $activePaymentMethods = apply_filters('fluent_cart/checkout_active_payment_methods', $activePaymentMethods, [
782 'cart' => $this->cart
783 ]);
784
785 if (!$selectedPaymentMethod && !empty($activePaymentMethods)) {
786 $selectedPaymentMethod = $activePaymentMethods[0] ? $activePaymentMethods[0]->getMeta('route') : '';
787 }
788
789 $checkoutMethodStyle = $this->storeSettings->get('checkout_method_style', 'logo');
790
791 ?>
792 <div id="fluent_payment_methods" class="fluent_payment_methods">
793 <div class="fct_checkout_form_section" aria-labelledby="payment_methods_label" role="radiogroup">
794 <div class="fct_form_section_header">
795 <h4 id="payment_methods_label" class="fct_form_section_header_label">
796 <?php esc_html_e('Payment', 'fluent-cart'); ?>
797 </h4>
798 </div>
799 <div class="fct_form_section_body">
800 <div
801 class="fct_payment_methods_list fct_payment_method_mode_<?php echo esc_attr($checkoutMethodStyle); ?>">
802 <?php if (!empty($activePaymentMethods)): ?>
803 <?php foreach ($activePaymentMethods as $method): ?>
804 <?php
805 $isSelected = ($selectedPaymentMethod === $method->getMeta('route'));
806
807 $this->renderPaymentMethod($method, [
808 'selected_id' => $selectedPaymentMethod,
809 'style' => $checkoutMethodStyle,
810 'aria_checked' => $isSelected ? 'true' : 'false',
811 'role' => 'radio'
812 ]);
813 ?>
814 <?php endforeach; ?>
815 <?php else: ?>
816 <?php
817 $emptyText = esc_html__('No Payment method is activated for this site yet.', 'fluent-cart');
818 if (current_user_can('manage_options')) {
819 $emptyText .= '<a href="' . esc_url(URL::getDashboardUrl('settings/payments')) . '" target="_blank">' . esc_html__('Activate from settings.', 'fluent-cart') . '</a>';
820 }
821 echo '<div class="fct-empty-state">' . wp_kses_post($emptyText) . '</div>';
822 ?>
823 <?php endif; ?>
824 </div>
825 </div>
826 </div>
827 </div>
828 <?php
829 }
830
831 public function renderCheckoutButton($atts = '')
832 {
833
834 $placeOrderButtonText = apply_filters('fluent_cart/checkout_page_order_button_text', __('Place order', 'fluent-cart'));
835 $attributes = [
836 'type' => 'submit',
837 'class' => 'fct_place_order_btn large',
838 'id' => 'fluent_cart_order_btn',
839 'data-fluent-cart-checkout-page-checkout-button' => '',
840 'data-value' => $placeOrderButtonText,
841 'disabled' => ''
842 ];
843
844 // Parse $atts using WordPress shortcode_atts or wp_parse_args
845 if (!empty($atts)) {
846 // Extract attributes from string
847 $parsed = shortcode_parse_atts($atts);
848
849 if (isset($parsed['class'])) {
850 $attributes['class'] .= ' ' . $parsed['class'];
851 unset($parsed['class']);
852 }
853
854 $attributes = array_merge($attributes, $parsed);
855 }
856
857 ?>
858 <div class="fct_place_order_btn_wrap">
859 <button <?php RenderHelper::renderAtts($attributes); ?>>
860 <?php echo esc_html($placeOrderButtonText); ?>
861 </button>
862 </div>
863 <?php
864 }
865
866 protected function renderPaymentMethod($method, $config = [])
867 {
868 $route = $method->getMeta('route');
869 $methodTitle = $method->getMeta('title');
870 $methodStyle = Arr::get($config, 'style', 'logo');
871
872 $inputAttributes = array_filter([
873 'class' => 'form-radio-input',
874 'type' => 'radio',
875 'name' => '_fct_pay_method',
876 'id' => 'fluent_cart_payment_method_' . $route,
877 'value' => $route,
878 'required' => true,
879 'checked' => $route === Arr::get($config, 'selected_id', '') ? 'true' : '',
880 'role' => Arr::get($config, 'role', 'radio'),
881 'aria-checked' => Arr::get($config, 'aria_checked', 'false'),
882 ]);
883
884 $wrapperClass = $methodStyle === 'logo' ? 'fct_payment_method_logo' : 'fct_payment_method';
885
886 $wrapperAttributes = [
887 'class' => $wrapperClass . ' ' . 'fct_payment_method_wrapper fct_payment_method_' . $route,
888 'tabindex' => '0',
889 'role' => 'presentation'
890 ];
891
892 ?>
893 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
894 <span class="fct-payment-method-loader">
895 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
896 <circle cx="12" cy="12" r="10" opacity="0.2" fill="none" stroke="currentColor" stroke-miterlimit="10"
897 stroke-width="2.5"></circle>
898
899 <path d="m12,2c5.52,0,10,4.48,10,10" fill="none" stroke="currentColor" stroke-linecap="round"
900 stroke-miterlimit="10" stroke-width="2.5">
901 <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="0.5s"
902 from="0 12 12" to="360 12 12" repeatCount="indefinite"></animateTransform>
903 </path>
904 </svg>
905 </span>
906
907 <input <?php RenderHelper::renderAtts($inputAttributes); ?> />
908 <label for="<?php echo esc_attr('fluent_cart_payment_method_' . $route); ?>">
909 <?php
910 if ($methodStyle === 'logo') {
911 $method->prepare('logo', $this->hasSubscription);
912 } else {
913 $method->prepare('radio', $this->hasSubscription);
914 }
915 ?>
916
917 <?php echo esc_html($methodTitle); ?>
918 </label>
919 <?php if ($method->getMeta('instructions')): ?>
920 <div class="fct_payment_method_instructions" style="display: none;">
921 <?php echo wp_kses_post($method->getMeta('instructions')); ?>
922 </div>
923 <?php endif; ?>
924 <div class="fluent-cart-checkout_embed_payment_wrapper">
925 <?php
926 $pmContext = [
927 'route' => $route,
928 'method_title' => $methodTitle,
929 'method_style' => $methodStyle,
930 ];
931 $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.');
932 $paymentMethodClass = apply_filters('fluent_cart/payment_method_list_class', $paymentMethodClass, $pmContext);
933
934 ?>
935 <div class="<?php echo "fluent-cart-checkout_embed_payment_container fluent-cart-checkout_embed_payment_container_" . esc_attr($route . ' ' . $paymentMethodClass); ?>"
936 aria-hidden="true">
937 <?php do_action(
938 'fluent_cart/checkout_embed_payment_method_content',
939 [
940 'method' => $method,
941 'cart' => $this->cart,
942 'route' => $route
943 ]
944 ); ?>
945 </div>
946 </div>
947 </div>
948 <?php
949 }
950
951 private function maybeRearrangeAddressFields($fields)
952 {
953 if (isset($fields['city']) && isset($fields['postcode'])) {
954 $cityField = $fields['city'];
955 $postcodeField = $fields['postcode'];
956 unset($fields['city'], $fields['postcode']);
957 $fields['city_postal'] = [
958 'type' => 'sub_section',
959 'wrapper_class' => 'fct_2_columns fct_checkout_city_postcode',
960 'fields' => [
961 'city' => $cityField,
962 'postcode' => $postcodeField
963 ],
964 ];
965 }
966
967 if (isset($fields['phone'])) {
968 $phoneField = $fields['phone'];
969 unset($fields['phone']);
970 $fields['phone'] = $phoneField;
971 }
972
973 if (isset($fields['company_name'])) {
974 $companyField = $fields['company_name'];
975 unset($fields['company_name']);
976 $fields['company_name'] = $companyField;
977 }
978
979 return $fields;
980 }
981
982
983 public function agreeTerms($atts = [])
984 {
985 if (!CheckoutFieldsSchema::isTermsVisible()) {
986 return;
987 }
988
989 $termsText = CheckoutFieldsSchema::getTermsText();
990 $sectionId = 'agree_terms_section';
991 $extraClass = Arr::get($atts, 'wrapper_atts') ? '' : 'fct-has-default-font-size';
992 $title = Arr::get($atts, 'title');
993
994 ?>
995 <div class="fct_checkout_form_section <?php echo esc_attr($extraClass); ?>" role="group" aria-labelledby="agree_terms_label" data-fct-checkout-form-section>
996 <div class="fct_form_section_body">
997 <div class="fct_checkout_agree_terms">
998 <div>
999 <label for="agree_terms" class="fct_input_label fct_input_label_checkbox">
1000 <input data-fluent-cart-agree-terms="yes" type="checkbox" class="fct-input fct-input-checkbox"
1001 id="agree_terms" name="agree_terms" value="yes" required aria-required="true"
1002 aria-label="<?php echo esc_attr($termsText); ?>">
1003 <?php
1004 if (!empty($title)) {
1005 echo esc_html($title);
1006 } else {
1007 echo wp_kses_post($termsText);
1008 } ?>
1009 </label>
1010 <span
1011 id="<?php echo esc_attr($sectionId); ?>"
1012 data-fluent-cart-checkout-page-form-error=""
1013 class="fct_form_error fct_error_<?php echo esc_attr($sectionId); ?>"
1014 role="alert"
1015 aria-live="polite"
1016 ></span>
1017 </div>
1018 </div>
1019 </div>
1020 </div>
1021 <?php }
1022
1023 private function renderSummaryFragment()
1024 {
1025 (new CartSummaryRender($this->cart))->render(false);
1026 }
1027 }
1028