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 / ModalCheckoutRenderer.php

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

842 lines 37.9 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\Helpers\Helper;
13 use FluentCart\App\Models\Cart;
14 use FluentCart\App\Models\ProductVariation;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16 use FluentCart\App\Services\URL;
17 use FluentCart\App\Vite;
18 use FluentCart\Framework\Support\Arr;
19 use FluentCart\App\Models\ProductMeta;
20
21 class ModalCheckoutRenderer
22 {
23 private $cart;
24
25 private $requireShipping;
26
27 private $hasSubscription = false;
28
29 private $config = [];
30
31 private $storeSettings;
32
33 private $productMeta;
34
35 private $matchedVariation;
36
37 private $checkoutRenderer;
38
39 private $cartSummaryRenderer;
40
41 private $customer;
42
43 /**
44 * Memoised result of the before_payment_methods placement filter. Null until
45 * resolved; see beforePaymentMethodsPlacement() for why it is asked only once.
46 *
47 * @var string|null
48 */
49 private $paymentMethodsHookPlacement = null;
50
51 public function __construct(Cart $cart, $config = [])
52 {
53 $this->cart = $cart;
54 $this->config = $config;
55 $this->requireShipping = $cart->requireShipping();
56 $this->storeSettings = new StoreSettings();
57 $this->checkoutRenderer = new CheckoutRenderer($cart);
58 $this->cartSummaryRenderer = new CartSummaryRender($cart);
59 $this->hasSubscription = $cart->hasSubscription();
60 $this->customer = CustomerResource::getCurrentCustomer();
61
62 $this->productMeta = ProductMeta::query()->where('object_id', Arr::get($this->cart->cart_data, '0.post_id'))->where('meta_key', 'license_settings')->first();
63
64 $variations = $this->productMeta->meta_value['variations'] ?? [];
65
66 $cartItem = $this->cart->cart_data[0] ?? [];
67 $objectId = $cartItem['object_id'] ?? null;
68
69 $this->matchedVariation = $objectId && isset($variations[$objectId])
70 ? $variations[$objectId]
71 : null;
72 }
73
74 public function getFragment($fragmentName)
75 {
76 $maps = [
77 'payment_methods' => 'renderPaymentMethods',
78 'summary_group' => 'renderSummaryGroup',
79 'checkout_summary' => 'renderCheckoutSummary'
80 ];
81
82 if(isset($maps[$fragmentName])) {
83 ob_start();
84 $this->{$maps[$fragmentName]}();
85 return ob_get_clean();
86 }
87
88 return '';
89
90 }
91
92 public function render()
93 {
94 ?>
95 <div class="fct-checkout-modal-container" data-fct-checkout-modal-container style="opacity: 0; visibility: hidden;">
96 <div class="fct-checkout-modal" data-fct-checkout-modal>
97 <div class="fct-checkout-modal-loader" data-fct-checkout-modal-loader>
98 <div class="fct-checkout-modal-loader-spinner"></div>
99 </div>
100 <button class="fct-checkout-modal-close" data-fct-checkout-modal-close aria-label="<?php echo esc_attr__('Close checkout', 'fluent-cart');?>">
101 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
102 <line x1="18" y1="6" x2="6" y2="18"></line>
103 <line x1="6" y1="6" x2="18" y2="18"></line>
104 </svg>
105 </button>
106 <div class="fct-checkout-modal-content">
107 <?php $this->renderIframe();?>
108 </div>
109 </div>
110 </div>
111 <?php
112 }
113
114 public function renderIframe()
115 {
116 ?>
117 <iframe class="fct-checkout-modal-iframe" data-fct-checkout-modal-iframe title="<?php echo esc_attr__('Checkout', 'fluent-cart');?>" frameborder="0" allowtransparency="true" spellcheck="false"></iframe>
118 <?php
119 }
120
121 public function renderForm()
122 {
123 // Without a billing country in checkout_data, the tax recalc below
124 // produces no per-item tax. Pre-fill from the current customer's
125 // primary billing address (if any) so the per-item tax badge can
126 // render on the very first modal open instead of only after the
127 // customer interacts with the address form.
128 $this->maybePopulateFormDataFromCustomer();
129
130 // Trigger the recalc only when the cart hasn't been taxed yet. Once
131 // line_meta.tax_config is on the item, the badge already renders from
132 // existing data — another recalc here would just duplicate work
133 // (Tax + Shipping each call $cart->save() on this action).
134 // Payload shape matches Cart::addItem() so every handler receives the
135 // data it expects.
136 if (empty(Arr::get($this->cart->cart_data, '0.line_meta.tax_config'))) {
137 do_action('fluent_cart/cart/cart_data_items_updated', [
138 'cart' => $this->cart,
139 'scope' => 'modal_open',
140 'scope_data' => null,
141 ]);
142 }
143 ?>
144 <div class="fct-modal-checkout-form-wrapper" data-fluent-cart-checkout-page>
145 <form
146 class="fct-modal-checkout-form"
147 action="<?php echo esc_url(home_url('/')); ?>"
148 method="POST"
149 data-fluent-cart-checkout-page-checkout-form
150 enctype="multipart/form-data"
151 aria-label="<?php esc_attr_e('Checkout Form', 'fluent-cart'); ?>"
152 >
153 <div class="fct-modal-checkout-form-inner" data-fct-modal-checkout-form-inner>
154 <?php $this->renderCheckoutDetails(); ?>
155 <?php $this->renderCheckoutBilling(); ?>
156 </div>
157 </form>
158 </div>
159 <?php
160 }
161
162
163 private function maybePopulateFormDataFromCustomer()
164 {
165 if (!$this->customer) {
166 return;
167 }
168
169 $checkoutData = is_array($this->cart->checkout_data) ? $this->cart->checkout_data : [];
170 $formData = Arr::get($checkoutData, 'form_data', []);
171
172 // Never clobber an answer the customer has actively chosen; only
173 // pre-fill when the cart has no billing country yet.
174 if (!empty(Arr::get($formData, 'billing_country'))) {
175 return;
176 }
177
178 $primaryBilling = $this->customer->primary_billing_address;
179 if (!$primaryBilling) {
180 return;
181 }
182
183 foreach ($primaryBilling->getFormattedDataForCheckout('billing_') as $key => $value) {
184 if (!isset($formData[$key]) || $formData[$key] === '' || $formData[$key] === null) {
185 $formData[$key] = $value;
186 }
187 }
188
189 $checkoutData['form_data'] = $formData;
190 $this->cart->checkout_data = $checkoutData;
191 // No explicit save — the recalc action that follows persists this
192 // together with the newly computed tax data, avoiding a double write.
193 }
194
195 /**
196 * Where `fluent_cart/before_payment_methods` fires in the modal.
197 *
198 * 'payment' (default) wraps the payment-method list, matching CheckoutRenderer.
199 * 'details' restores the pre-fix position in the address/shipping pane, for a
200 * store whose add-on positioned itself around that spot.
201 *
202 * Resolved ONCE per renderer and memoised. The two call sites are mutually
203 * exclusive, so the hook must fire exactly once — but that only holds if both
204 * sites agree, and a filter is free to be stateful or context-sensitive. Asking
205 * it twice would let it answer differently and either fire the hook twice or
206 * swallow it entirely. Ask once, remember the answer.
207 *
208 * @return string 'payment'|'details'
209 */
210 private function beforePaymentMethodsPlacement(): string
211 {
212 if ($this->paymentMethodsHookPlacement !== null) {
213 return $this->paymentMethodsHookPlacement;
214 }
215
216 $placement = apply_filters('fluent_cart/modal_checkout/before_payment_methods_placement', 'payment', [
217 'cart' => $this->cart,
218 ]);
219
220 // Anything unrecognised falls back to the correct placement rather than
221 // silently dropping the hook.
222 $this->paymentMethodsHookPlacement = $placement === 'details' ? 'details' : 'payment';
223
224 return $this->paymentMethodsHookPlacement;
225 }
226
227 public function renderCheckoutDetails()
228 {
229 ?>
230 <div class="fct-modal-checkout-details">
231 <!-- Stable swap target: tax recalculation re-renders the item card
232 here via the checkout_summary fragment. -->
233 <div data-fct-modal-checkout-summary>
234 <?php $this->renderCheckoutSummary(); ?>
235 </div>
236
237 <?php $this->checkoutRenderer->renderAddressFields(); ?>
238
239 <div class="fct_checkout_shipping_methods <?php echo $this->requireShipping ? '' : 'is-hidden' ?>">
240 <?php $this->checkoutRenderer->renderShippingOptions(); ?>
241 </div>
242
243 <?php
244 /**
245 * The position `fluent_cart/before_payment_methods` used to
246 * occupy in the modal. Given its own name so an add-on that
247 * genuinely wants this pane — under shipping, above the terms
248 * — has a stable place to render, instead of relying on a hook
249 * whose name promises it sits before the payment methods.
250 *
251 * @param array $data ['cart' => Cart]
252 */
253 do_action('fluent_cart/modal_checkout/after_shipping_methods', ['cart' => $this->cart]);
254
255 // Opt-in restoration of the old placement, for a store whose
256 // add-on positioned itself around the previous spot. Mutually
257 // exclusive with the call site in renderCheckoutBilling().
258 if ($this->beforePaymentMethodsPlacement() === 'details') {
259 do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]);
260 }
261 ?>
262
263 <?php $this->checkoutRenderer->agreeTerms(); ?>
264
265 <?php $this->renderSummaryGroup(); ?>
266
267 <?php //$this->renderCheckoutReviews();?>
268 </div>
269 <?php
270 }
271
272 public function renderCheckoutSummary()
273 {
274 $title = Arr::get($this->cart->cart_data, '0.title', '');
275 $postTitle = Arr::get($this->cart->cart_data, '0.post_title', '');
276 $subTotal = Helper::toDecimal(Arr::get($this->cart->cart_data, '0.subtotal', 0));
277 $couponDiscount = (int) Arr::get($this->cart->cart_data, '0.coupon_discount', 0);
278 $subtotalRaw = (int) Arr::get($this->cart->cart_data, '0.subtotal', 0);
279 $lineTotalRaw = (int) Arr::get($this->cart->cart_data, '0.line_total', 0);
280 $hasCouponDiscount = $couponDiscount > 0 && $lineTotalRaw < $subtotalRaw;
281 $media = Arr::get($this->cart->cart_data, '0.featured_media', '');
282
283 // Mirror CartItemRenderer's event payload so the shared line-item hooks
284 // (e.g. the per-item tax breakdown) also fire in modal checkout.
285 $lineItemEventInfo = [
286 'item' => Arr::get($this->cart->cart_data, '0', []),
287 'cart' => $this->cart,
288 'product' => null,
289 'variant' => null,
290 ];
291
292 ?>
293 <div class="fct-modal-checkout-summary">
294 <div class="fct-modal-cs-img">
295 <img src="<?php echo esc_url($media);?>" alt="<?php echo esc_attr($postTitle);?>">
296 </div>
297
298 <div class="fct-modal-cs-content">
299 <div class="fct-modal-cs-line-item">
300 <div class="fct-modal-cs-item-content">
301 <h2 class="fct-modal-cs-item-name">
302 <?php echo esc_html($postTitle);?>
303 </h2>
304 <h3 class="fct-modal-cs-item-variation">
305 - <?php echo esc_html($title);?>
306 </h3>
307 </div>
308
309 <?php if ($hasCouponDiscount) : ?>
310 <div class="fct-modal-cs-price-wrapper">
311 <span class="fct-modal-cs-line-price fct-modal-cs-line-price--original" aria-label="<?php esc_attr_e('Original price', 'fluent-cart'); ?>">
312 <?php echo esc_html(Helper::toDecimal($subtotalRaw)); ?>
313 </span>
314 <span class="fct-modal-cs-line-price fct-modal-cs-line-price--discounted" aria-label="<?php esc_attr_e('Discounted price', 'fluent-cart'); ?>">
315 <?php echo esc_html(Helper::toDecimal($lineTotalRaw)); ?>
316 </span>
317 </div>
318 <?php else : ?>
319 <span class="fct-modal-cs-line-price">
320 <?php echo esc_html($subTotal); ?>
321 </span>
322 <?php endif; ?>
323 <?php do_action('fluent_cart/cart/line_item/after_total', $lineItemEventInfo); ?>
324 </div>
325
326 <?php do_action('fluent_cart/cart/line_item/footer_start', $lineItemEventInfo); ?>
327
328 <?php $this->renderPaymentTypeInfo(); ?>
329
330 <?php if($this->matchedVariation) :?>
331 <div class="fct-modal-cs-license">
332 <?php
333 $limit = Arr::get($this->matchedVariation, 'activation_limit');
334
335 if (empty($limit) || (int) $limit === 0) {
336 echo esc_html__('Unlimited Activation', 'fluent-cart');
337 } else {
338 echo esc_html(sprintf(__('Activation limit %d', 'fluent-cart'), $limit));
339 }
340 ?>
341 </div>
342 <?php endif;?>
343
344
345 </div>
346 </div>
347
348 <?php
349
350 }
351
352 public function renderPaymentTypeInfo() {
353 $otherInfo = Arr::get($this->cart->cart_data, '0.other_info', []);
354 $paymentType = Arr::get($otherInfo, 'payment_type', '');
355 $itemPrice = Arr::get($this->cart->cart_data, '0.unit_price', 0);
356
357 if ($paymentType === 'subscription') {
358 $subscriptionInfo = Helper::generateSubscriptionInfo($otherInfo, $itemPrice);
359 $setupFeeInfo = Helper::generateSetupFeeInfo($otherInfo);
360 $trialInfo = Helper::generateTrialInfo($otherInfo);
361 ?>
362 <div class="fct-modal-cs-payment-info">
363 <span class="sr-only">
364 <?php esc_html_e('Payment information', 'fluent-cart'); ?>
365 </span>
366
367 <span>
368 <?php echo wp_kses($subscriptionInfo, ['del' => true]); ?>&#44;
369 </span>
370
371 <?php if ($trialInfo): ?>
372 <span class="trial-days">
373 <?php echo esc_html($trialInfo); ?>&#44;
374 </span>
375 <?php endif; ?>
376
377 <?php if (!empty($setupFeeInfo)): ?>
378 <span class="setup-fee">
379 <?php echo esc_html($setupFeeInfo); ?>
380 </span>
381 <?php endif; ?>
382 </div>
383 <?php
384 return;
385 }
386
387 $quantity = (int) Arr::get($this->cart->cart_data, '0.quantity', 1);
388 if ($quantity < 2) {
389 return;
390 }
391
392 $lineItemEventInfo = [
393 'item' => Arr::get($this->cart->cart_data, '0', []),
394 'cart' => $this->cart,
395 'product' => null,
396 'variant' => null,
397 ];
398 ?>
399 <div class="fct-modal-cs-payment-info">
400 <?php
401 /* translators: %1$s: formatted unit price */
402 printf(esc_html__('%1$s each', 'fluent-cart'), esc_html(Helper::toDecimal($itemPrice)));
403 ?>
404 <?php do_action('fluent_cart/cart/line_item/unit_price_hint', $lineItemEventInfo); ?>
405 </div>
406 <?php
407 }
408
409 public function renderPromoCode() {
410 ?>
411 <div class="fct-modal-checkout-promo-code">
412 <div class="fct-modal-input-control-wrap" data-fct-modal-input-control-wrap>
413 <button type="button" class="fct-modal-input-toggle-btn" data-fct-modal-input-toggle-btn>
414 <?php echo esc_html__('Have a promotional code?', 'fluent-cart');?>
415 </button>
416
417 <div class="fct-modal-input-controls fct-hidden" data-fct-modal-input-controls>
418 <span class="fct-input-icon">
419 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
420 <path fill="currentColor" d="M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0 160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17l0 80c0 13.3 10.7 24 24 24l80 0c13.3 0 24-10.7 24-24l0-40 40 0c13.3 0 24-10.7 24-24l0-40 40 0c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zM376 96a40 40 0 1 1 0 80 40 40 0 1 1 0-80z"/></svg>
421 </span>
422
423 <input
424 class="fct-input-field"
425 id="coupon"
426 type="text"
427 name="coupon"
428 placeholder="<?php echo esc_attr__('Enter promotion code', 'fluent-cart');?>"
429 >
430
431
432 <button type="button" class="fct-input-submit-btn" data-fluent-cart-checkout-page-coupon-validate>
433 <?php echo esc_html__('Apply', 'fluent-cart');?>
434 </button>
435 </div>
436 </div>
437 </div>
438 <?php
439 }
440 public function renderSummaryGroup() {
441 ?>
442
443 <div class="fct-modal-checkout-summary-group" data-fct-modal-checkout-summary-group>
444 <?php
445 $this->cartSummaryRenderer->renderItemsFooter();
446 ?>
447 </div>
448
449 <?php
450
451 }
452
453
454 public function showCouponApplied()
455 {
456 $discounts = $this->cart->getDiscountLines();
457
458 if (!$discounts) {
459 return;
460 }
461
462 ?>
463
464 <div class="fct-modal-coupon-applied" data-fluent-cart-checkout-page-discount-container>
465 <?php foreach ($discounts as $couponCode => $discount_data): ?>
466 <div class="fct-coupon-applied-item">
467 <div class="fct-coupon-info">
468 <span class="fct-coupon">
469 <span class="fct-coupon-icon">
470 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M32.5 96l0 149.5c0 17 6.7 33.3 18.7 45.3l192 192c25 25 65.5 25 90.5 0L483.2 333.3c25-25 25-65.5 0-90.5l-192-192C279.2 38.7 263 32 246 32L96.5 32c-35.3 0-64 28.7-64 64zm112 16a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>
471 </span>
472
473 <?php echo esc_html($discount_data['formatted_title']) ?>
474
475 <a
476 href="#"
477 class="fct-remove-coupon"
478 data-remove-coupon
479 data-coupon="<?php echo esc_attr($couponCode); ?>"
480 >
481 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 384 512"><path d="M55.1 73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L147.2 256 9.9 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192.5 301.3 329.9 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.8 256 375.1 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192.5 210.7 55.1 73.4z"/></svg>
482 </a>
483 </span>
484 <span class="fct-coupon-text">
485 <?php echo esc_html__( 'Coupon discount', 'fluent-cart'); ?>
486 </span>
487 </div>
488 <div class="fct-coupon-price">
489 &#8211;<?php echo esc_html($discount_data['actual_formatted_discount']) ?>
490 </div>
491 </div>
492 <?php endforeach; ?>
493 </div>
494
495 <?php
496 }
497
498 public function renderCheckoutReviews() {
499 ?>
500 <div class="fct-modal-checkout-reviews-wrap">
501 <div class="fct-modal-checkout-reviews">
502 <div class="fct-reviews-picture">
503 <img width="40" height="40" src="https://images.pexels.com/photos/23885849/pexels-photo-23885849.jpeg" alt="">
504 </div>
505 <div class="fct-reviews-content">
506 <div class="fct-reviews-text">
507 I got the annual unlimited websites premium version and I'm upgrading to the Lifetime Unlimited Websites plan real soon. Building WordPress website
508 </div>
509 <div class="fct-reviews-meta">
510 <div class="fct-review-stars">
511 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
512 <path fill="currentColor" d="M309.5-18.9c-4.1-8-12.4-13.1-21.4-13.1s-17.3 5.1-21.4 13.1L193.1 125.3 33.2 150.7c-8.9 1.4-16.3 7.7-19.1 16.3s-.5 18 5.8 24.4l114.4 114.5-25.2 159.9c-1.4 8.9 2.3 17.9 9.6 23.2s16.9 6.1 25 2L288.1 417.6 432.4 491c8 4.1 17.7 3.3 25-2s11-14.2 9.6-23.2L441.7 305.9 556.1 191.4c6.4-6.4 8.6-15.8 5.8-24.4s-10.1-14.9-19.1-16.3L383 125.3 309.5-18.9z"/>
513 </svg>
514 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
515 <path fill="currentColor" d="M309.5-18.9c-4.1-8-12.4-13.1-21.4-13.1s-17.3 5.1-21.4 13.1L193.1 125.3 33.2 150.7c-8.9 1.4-16.3 7.7-19.1 16.3s-.5 18 5.8 24.4l114.4 114.5-25.2 159.9c-1.4 8.9 2.3 17.9 9.6 23.2s16.9 6.1 25 2L288.1 417.6 432.4 491c8 4.1 17.7 3.3 25-2s11-14.2 9.6-23.2L441.7 305.9 556.1 191.4c6.4-6.4 8.6-15.8 5.8-24.4s-10.1-14.9-19.1-16.3L383 125.3 309.5-18.9z"/>
516 </svg>
517 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
518 <path fill="currentColor" d="M309.5-18.9c-4.1-8-12.4-13.1-21.4-13.1s-17.3 5.1-21.4 13.1L193.1 125.3 33.2 150.7c-8.9 1.4-16.3 7.7-19.1 16.3s-.5 18 5.8 24.4l114.4 114.5-25.2 159.9c-1.4 8.9 2.3 17.9 9.6 23.2s16.9 6.1 25 2L288.1 417.6 432.4 491c8 4.1 17.7 3.3 25-2s11-14.2 9.6-23.2L441.7 305.9 556.1 191.4c6.4-6.4 8.6-15.8 5.8-24.4s-10.1-14.9-19.1-16.3L383 125.3 309.5-18.9z"/>
519 </svg>
520 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
521 <path fill="currentColor" d="M309.5-18.9c-4.1-8-12.4-13.1-21.4-13.1s-17.3 5.1-21.4 13.1L193.1 125.3 33.2 150.7c-8.9 1.4-16.3 7.7-19.1 16.3s-.5 18 5.8 24.4l114.4 114.5-25.2 159.9c-1.4 8.9 2.3 17.9 9.6 23.2s16.9 6.1 25 2L288.1 417.6 432.4 491c8 4.1 17.7 3.3 25-2s11-14.2 9.6-23.2L441.7 305.9 556.1 191.4c6.4-6.4 8.6-15.8 5.8-24.4s-10.1-14.9-19.1-16.3L383 125.3 309.5-18.9z"/>
522 </svg>
523 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
524 <path fill="currentColor" d="M309.5-18.9c-4.1-8-12.4-13.1-21.4-13.1s-17.3 5.1-21.4 13.1L193.1 125.3 33.2 150.7c-8.9 1.4-16.3 7.7-19.1 16.3s-.5 18 5.8 24.4l114.4 114.5-25.2 159.9c-1.4 8.9 2.3 17.9 9.6 23.2s16.9 6.1 25 2L288.1 417.6 432.4 491c8 4.1 17.7 3.3 25-2s11-14.2 9.6-23.2L441.7 305.9 556.1 191.4c6.4-6.4 8.6-15.8 5.8-24.4s-10.1-14.9-19.1-16.3L383 125.3 309.5-18.9z"/>
525 </svg>
526 </div>
527 <div class="fct-review-author-info">
528 <span class="fct-author-name">Bryson Suleiman,</span>
529 <span class="fct-author-role">Founder and WordPress Expert at Webbryson</span>
530 </div>
531 </div>
532 </div>
533 </div>
534 </div>
535
536 <?php
537
538 }
539
540
541 public function renderCheckoutBilling() {
542 $user = wp_get_current_user();
543 $formRender = new FormFieldRenderer();
544 $fullName = trim(
545 ($this->customer->first_name ?? '') . ' ' . ($this->customer->last_name ?? '')
546 );
547
548 if (!$fullName && $user) {
549 $fullName = $user->display_name;
550 }
551
552 $fieldsSchema = CheckoutFieldsSchema::getFieldsSettings();
553
554 $fullNameField = Arr::get($fieldsSchema, 'basic_info.full_name', []);
555 $emailField = Arr::get($fieldsSchema, 'basic_info.email', []);
556 $isRequiredFullName = Arr::get($fullNameField, 'required', 'no') === 'yes' ? 'yes' : '';
557 $isRequiredEmail = Arr::get($emailField, 'required', 'no') === 'yes' ? 'yes' : '';
558
559 ?>
560 <div class="fct-modal-checkout-billing-wrap">
561 <!-- Account Details -->
562 <div class="fct-modal-account-details" data-fct-checkout-form-section>
563 <div class="fct-modal-form-info">
564 <div class="fct-modal-form-field col-6">
565 <?php
566 $formRender->renderField([
567 'label' => esc_attr__('Full name', 'fluent-cart') . ($isRequiredFullName ? ' *' : ''),
568 'id' => 'billing_full_name',
569 'type' => 'text',
570 'placeholder' => __('Jon Doe', 'fluent-cart'),
571 'name' => 'billing_full_name',
572 'autocomplete' => 'given-name',
573 'aria-label' => esc_attr__('Full name', 'fluent-cart'),
574 'required' => $isRequiredFullName,
575 'value' => $fullName
576 ]);
577 ?>
578 </div>
579 <div class="fct-modal-form-field col-6">
580 <?php
581 $formRender->renderField([
582 'label' => esc_attr__('Email address', 'fluent-cart') . ($isRequiredEmail ? ' *' : ''),
583 'id' => 'billing_email',
584 'type' => 'text',
585 'placeholder' => 'jon.doe@example.com',
586 'name' => 'billing_email',
587 'autocomplete' => 'email',
588 'required' => $isRequiredEmail,
589 'aria-label' => esc_attr__('Email address', 'fluent-cart'),
590 'value' => $this->customer->email ?? $user->user_email,
591 'disabled' => (bool)($this->customer->email ?? $user->user_email)
592 ]);
593 ?>
594 </div>
595
596 <!-- <div class="fct-modal-form-field col-3">-->
597 <!-- --><?php
598 // $formRender->renderField([
599 // 'label' => __('Last name', 'fluent-cart'),
600 // 'id' => 'billing_last_name',
601 // 'type' => 'text',
602 // 'placeholder' => __('Doe', 'fluent-cart'),
603 // 'name' => 'billing_last_name',
604 // 'autocomplete' => 'given-name',
605 // 'required' => true,
606 // 'value' => $this->customer->last_name ?? ''
607 // ]);
608 // ?>
609 <!-- </div>-->
610 </div>
611
612 <?php $this->checkoutRenderer->renderCreateAccountField(); ?>
613
614 <!-- Error Message -->
615 <div class="fct_form_error fct_error_billing_personal_information_section"></div>
616 </div>
617
618 <!-- Billing Details -->
619 <div class="fct-modal-billing-details">
620 <header class="fct-modal-billing-header">
621 <h4 class="fct-modal-billing-title">
622 <?php echo esc_html__('Billing information', 'fluent-cart');?>
623 </h4>
624 </header>
625
626 <?php
627 /**
628 * Fired here, wrapping the payment methods, exactly as
629 * CheckoutRenderer does. It used to fire from
630 * renderCheckoutDetails() — the address/shipping pane — which
631 * is a different part of the modal entirely, so anything
632 * hooked to it (the saved-payment-method picker, Turnstile)
633 * rendered detached from the methods it belongs to. A hook
634 * named "before payment methods" has to fire before the
635 * payment methods.
636 */
637 if ($this->beforePaymentMethodsPlacement() === 'payment') {
638 do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]);
639 }
640 ?>
641
642 <div class="fct_checkout_payment_methods" data-fluent-cart-checkout-payment-methods>
643 <?php $this->renderPaymentMethods(); ?>
644 </div>
645
646 <?php
647 // The modal never fired this at all, so add-ons rendering
648 // below the methods (the save-my-card consent box) were
649 // simply absent from quick checkout.
650 do_action('fluent_cart/after_payment_methods', ['cart' => $this->cart]);
651 ?>
652
653 <div class="fct-modal-checkout-btn-wrap">
654 <?php (new CheckoutRenderer($this->cart))->renderCheckoutButton(); ?>
655 </div>
656 </div>
657 </div>
658
659 <?php
660
661 }
662
663 public function renderPaymentMethods()
664 {
665 if ($this->cart->getEstimatedTotal() <= 0) {
666 if (!$this->cart->hasSubscription() || $this->cart->getEstimatedRecurringTotal() <= 0) {
667 return '';
668 }
669 }
670
671 $selectedPaymentMethod = Arr::get($this->cart->checkout_data, 'form_data._fct_pay_method', '');
672 $activePaymentMethods = PaymentMethods::getActiveMethodInstance($this->cart);
673 $hadActiveMethods = !empty($activePaymentMethods);
674
675 $activePaymentMethods = apply_filters('fluent_cart/checkout_active_payment_methods', $activePaymentMethods, [
676 'cart' => $this->cart
677 ]);
678
679 // filter the active payment methods to only include the selected payment method
680 $selectedModalPaymentMethod = apply_filters('fluent_cart/modal_checkout/filter_active_payment_methods', []);
681 if (!empty($selectedModalPaymentMethod)) {
682 $activePaymentMethods = array_filter($activePaymentMethods, function ($method) use ($selectedModalPaymentMethod) {
683 return in_array($method->getMeta('route'), $selectedModalPaymentMethod);
684 });
685 }
686
687 if (!$selectedPaymentMethod && !empty($activePaymentMethods)) {
688 // reset() not [0] — array_filter above preserves keys, so key 0 may be gone.
689 $firstMethod = reset($activePaymentMethods);
690 $selectedPaymentMethod = $firstMethod ? $firstMethod->getMeta('route') : '';
691 }
692
693 $checkoutMethodStyle = $this->storeSettings->get('checkout_method_style', 'logo');
694
695 ?>
696 <div id="fluent_payment_methods" class="fct_modal_payment_methods fluent_payment_methods">
697 <!-- Payment Methods -->
698 <div class="fct_payment_methods_list fct_payment_method_mode_<?php echo esc_attr($checkoutMethodStyle); ?>">
699 <?php if (!empty($activePaymentMethods)): ?>
700 <?php foreach ($activePaymentMethods as $method) {
701 $isSelected = ($selectedPaymentMethod === $method->getMeta('route'));
702
703 $this->renderPaymentMethod($method, [
704 'selected_id' => $selectedPaymentMethod,
705 'style' => $checkoutMethodStyle,
706 'aria_checked' => $isSelected ? 'true' : 'false',
707 'role' => 'radio'
708 ]);
709 } ?>
710 <?php else: ?>
711 <?php
712 if ($hadActiveMethods) {
713 $emptyText = esc_html__('None of the available payment methods can process this order. Please contact the store.', 'fluent-cart');
714 } else {
715 $emptyText = esc_html__('No Payment method is activated for this site yet.', 'fluent-cart');
716 }
717 if (current_user_can('manage_options')) {
718 $emptyText .= '<a href="' . esc_url(URL::getDashboardUrl('settings/payments')) . '" target="_blank">' . esc_html__('Activate from settings.', 'fluent-cart') . '</a>';
719 }
720 echo '<div class="fct-empty-state">' . wp_kses_post($emptyText) . '</div>';
721 ?>
722 <?php endif; ?>
723 </div>
724
725 <!-- Payment Method Content -->
726 <div class="fct_payment_method_contents">
727 <?php foreach ($activePaymentMethods as $method) {
728 $this->renderPaymentMethodContent($method, [
729 'style' => $checkoutMethodStyle
730 ]);
731 } ?>
732 </div>
733
734
735
736 </div>
737 <?php
738 }
739
740
741 public function renderPaymentMethod($method, $config = [])
742 {
743 $route = $method->getMeta('route');
744 $methodTitle = $method->getMeta('title');
745 $methodStyle = Arr::get($config, 'style', 'logo');
746
747 $inputAttributes = array_filter([
748 'class' => 'form-radio-input',
749 'type' => 'radio',
750 'name' => '_fct_pay_method',
751 'id' => 'fluent_cart_payment_method_' . $route,
752 'value' => $route,
753 'required' => true,
754 'checked' => $route === Arr::get($config, 'selected_id', '') ? 'true' : '',
755 'role' => Arr::get($config, 'role', 'radio'),
756 'aria-checked' => Arr::get($config, 'aria_checked', 'false'),
757 ]);
758
759 $wrapperClass = $methodStyle === 'logo' ? 'fct_payment_method_logo' : 'fct_payment_method';
760
761 $wrapperAttributes = [
762 'class' => $wrapperClass . ' ' . 'fct_payment_method_wrapper fct_payment_method_' . $route,
763 'tabindex' => '0',
764 'role' => 'presentation'
765 ];
766
767 ?>
768 <div <?php RenderHelper::renderAtts($wrapperAttributes); ?>>
769 <span class="fct-payment-method-loader">
770 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
771 <circle cx="12" cy="12" r="10" opacity="0.2" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2.5"></circle>
772
773 <path d="m12,2c5.52,0,10,4.48,10,10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2.5">
774 <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="0.5s" from="0 12 12" to="360 12 12" repeatCount="indefinite"></animateTransform>
775 </path>
776 </svg>
777 </span>
778
779 <input <?php RenderHelper::renderAtts($inputAttributes); ?>/>
780
781 <label for="<?php echo esc_attr('fluent_cart_payment_method_' . $route); ?>">
782 <?php
783 if ($methodStyle === 'logo') {
784 $method->prepare('logo', $this->hasSubscription);
785 } else {
786 $method->prepare('radio', $this->hasSubscription);
787 }
788 ?>
789
790 <?php echo esc_html($methodTitle); ?>
791 </label>
792 </div>
793 <?php
794 }
795
796 public function renderPaymentMethodContent($method, $config = []) {
797 $route = $method->getMeta('route');
798 $methodTitle = $method->getMeta('title');
799 $methodStyle = Arr::get($config, 'style', 'logo');
800 $pmContext = [
801 'route' => $route,
802 'method_title' => $methodTitle,
803 'method_style' => $methodStyle,
804 ];
805 $paymentMethodClass = '';
806 $paymentMethodClass = apply_filters('fluent_cart/payment_method_list_class', $paymentMethodClass, $pmContext);
807
808 ?>
809 <div class="fluent-cart-checkout_embed_payment_wrapper">
810 <?php if ($method->getMeta('instructions')): ?>
811 <div class="fct-modal-payment-method-instructions">
812 <?php echo wp_kses_post($method->getMeta('instructions')); ?>
813 </div>
814 <?php endif; ?>
815
816 <div class="<?php echo "fluent-cart-checkout_embed_payment_container fluent-cart-checkout_embed_payment_container_" . esc_attr($route . ' ' . $paymentMethodClass); ?>"
817 aria-hidden="true">
818 <?php do_action(
819 'fluent_cart/checkout_embed_payment_method_content',
820 [
821 'method' => $method,
822 'cart' => $this->cart,
823 'route' => $route
824 ]
825 ); ?>
826 </div>
827 </div>
828
829 <?php
830 }
831
832
833
834
835
836
837
838
839
840
841 }
842