PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.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 All 51 releases
fluent-cart / app / Services / Renderer / ModalCheckoutRenderer.php

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

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