PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.0
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Helpers / Helper.php

Helper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.4.0, at app/Helpers/Helper.php

1,918 lines 62.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\Helpers;
4
5 use FluentCart\Api\Confirmation;
6 use FluentCart\Api\CurrencySettings;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\Api\Taxonomy;
9 use FluentCart\App\App;
10 use FluentCart\App\CPT\FluentProducts;
11 use FluentCart\App\Models\Customer;
12 use FluentCart\App\Models\ProductVariation;
13 use FluentCart\App\Models\User;
14 use FluentCart\App\Services\DateTime\DateTime;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16 use FluentCart\App\Services\Translations\TransStrings;
17 use FluentCart\App\Services\URL;
18 use FluentCart\Framework\Http\URL as BaseUrl;
19 use FluentCart\Framework\Support\Arr;
20
21 class Helper
22 {
23 const PRODUCT_TYPE_SIMPLE = 'simple';
24 const PRODUCT_TYPE_SIMPLE_VARIATION = 'simple_variations';
25 const PRODUCT_TYPE_ADVANCE_VARIATION = 'advanced_variations';
26
27 const INSTANT_CHECKOUT_URL_PARAM = 'fct_cart_hash';
28
29 const IN_STOCK = 'in-stock';
30 const OUT_OF_STOCK = 'out-of-stock';
31
32 const ROLE_CAPABILITY_PREFIX = 'fluent_cart/permissions/';
33
34 const USER_ROLE = 'fluent_cart_customer';
35
36 public static function getUidSerial()
37 {
38 static $id = 0;
39
40 $id = $id + 1;
41
42 return $id;
43
44 }
45
46 public static function getRestInfo()
47 {
48 $app = App::getInstance();
49
50 $ns = $app->config->get('app.rest_namespace');
51 $ver = $app->config->get('app.rest_version');
52
53 return [
54 'base_url' => self::getBaseRestUrl(),
55 'url' => self::getFullRestUrl($ns, $ver),
56 'nonce' => wp_create_nonce('wp_rest'),
57 'namespace' => $ns,
58 'version' => $ver,
59 ];
60 }
61
62
63 /**
64 * Get base rest url by examining the permalink.
65 *
66 * @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
67 *
68 * @return string
69 */
70 protected static function getBaseRestUrl()
71 {
72 if (get_option('permalink_structure')) {
73 return esc_url_raw(rest_url());
74 }
75
76 return esc_url_raw(
77 rtrim(get_site_url(), '/') . "/?rest_route=/"
78 );
79 }
80
81 /**
82 * Get the full rest url by examining the permalink
83 * (full means, including the namespace/version).
84 *
85 * @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
86 *
87 * @return string
88 */
89 protected static function getFullRestUrl($ns, $ver)
90 {
91 if (get_option('permalink_structure')) {
92 return esc_url_raw(rest_url($ns . '/' . $ver));
93 }
94
95 return esc_url_raw(
96 rtrim(get_site_url(), '/') . "/?rest_route=/{$ns}/{$ver}"
97 );
98 }
99
100
101 public static function shopConfig($key = false)
102 {
103 /**
104 * todo - need to review for more improvement : AR
105 */
106 $currencySettings = (fluentCart(CurrencySettings::class))->get();
107 $storeSettings = (new StoreSettings())->get([
108 'store_name', 'store_logo', 'weight_unit', 'dimension_unit'
109 ]);
110 $storeSettings['shipping_packages'] = self::getShippingPackages();
111
112 $settings = array_merge($currencySettings, $storeSettings);
113
114
115 if (!$key) {
116 return $settings;
117 }
118
119 if (is_array($key)) {
120 return Arr::only($settings, $key);
121 } else {
122 return Arr::get($settings, $key);
123 }
124
125 }
126
127 /**
128 * Convert weight between units using grams as base.
129 *
130 * @param float $value
131 * @param string $fromUnit
132 * @param string $toUnit
133 * @return float
134 */
135 public static function convertWeight($value, $fromUnit, $toUnit)
136 {
137 if ($fromUnit === $toUnit || !$value) {
138 return (float)$value;
139 }
140
141 $toGrams = [
142 'g' => 1,
143 'kg' => 1000,
144 'lbs' => 453.592,
145 'oz' => 28.3495,
146 ];
147
148 $fromFactor = isset($toGrams[$fromUnit]) ? $toGrams[$fromUnit] : 1;
149 $toFactor = isset($toGrams[$toUnit]) ? $toGrams[$toUnit] : 1;
150
151 $grams = $value * $fromFactor;
152 return round($grams / $toFactor, 6);
153 }
154
155 /**
156 * Get all shipping packages from store settings.
157 *
158 * @return array
159 */
160 public static function getShippingPackages()
161 {
162 static $packages = null;
163 if ($packages === null) {
164 $packages = fluent_cart_get_option('shipping_packages', []);
165 }
166 return $packages ?: [];
167 }
168
169 /**
170 * Get the default shipping package.
171 *
172 * @return array|null
173 */
174 public static function getDefaultPackage()
175 {
176 $packages = self::getShippingPackages();
177 foreach ($packages as $package) {
178 if (!empty($package['is_default'])) {
179 return $package;
180 }
181 }
182 return null;
183 }
184
185 /**
186 * Find a shipping package by slug.
187 *
188 * @param string $slug
189 * @return array|null
190 */
191 public static function getPackageBySlug($slug)
192 {
193 if (!$slug) {
194 return self::getDefaultPackage();
195 }
196 $packages = self::getShippingPackages();
197 foreach ($packages as $package) {
198 if (isset($package['slug']) && $package['slug'] === $slug) {
199 return $package;
200 }
201 }
202 return self::getDefaultPackage();
203 }
204
205 public static function invoiceSettings($key = false)
206 {
207 $settings = [
208 'invoice_prefix' => 'AS-',
209 ];
210 if (!$key) {
211 return $settings;
212 }
213
214 return Arr::get($settings, $key);
215 }
216
217 public static function getOrderStatuses()
218 {
219 $statuses = apply_filters_deprecated('fluent-cart/order_statuses', [
220 [
221 'on-hold' => __('On Hold', 'fluent-cart'),
222 'processing' => __('Processing', 'fluent-cart'),
223 'completed' => __('Completed', 'fluent-cart'),
224 //'archived' => __('Archived', 'fluent-cart'),
225 'cancelled' => __('Cancelled', 'fluent-cart'),
226 ], []
227 ], '1.3.16', 'fluent_cart/order_statuses', 'Use fluent_cart/order_statuses instead of fluent-cart/order_statuses.');
228
229 return apply_filters('fluent_cart/order_statuses', $statuses, []);
230 }
231
232 public static function getEditableOrderStatuses()
233 {
234 $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
235 [
236 'on-hold' => __('On Hold', 'fluent-cart'),
237 'processing' => __('Processing', 'fluent-cart'),
238 'completed' => __('Completed', 'fluent-cart'),
239 // 'archived' => __('Archived', 'fluent-cart'),
240 'cancelled' => __('Cancelled', 'fluent-cart')
241 ], []
242 ], '1.3.16', 'fluent_cart/editable_order_statuses', 'Use fluent_cart/editable_order_statuses instead of fluent-cart/editable_order_statuses.');
243
244 return apply_filters('fluent_cart/editable_order_statuses', $statuses, []);
245 }
246
247 public static function getEditableCustomerStatuses()
248 {
249 $statuses = apply_filters_deprecated('fluent-cart/editable_customer_statuses', [
250 [
251 'active' => __('Active', 'fluent-cart'),
252 'inactive' => __('Inactive', 'fluent-cart'),
253 ], []
254 ], '1.3.16', 'fluent_cart/editable_customer_statuses', 'Use fluent_cart/editable_customer_statuses instead of fluent-cart/editable_customer_statuses.');
255
256 return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []);
257 }
258
259 public static function getShippingStatuses()
260 {
261 $statuses = apply_filters_deprecated('fluent-cart/shipping_statuses', [
262 [
263 'unshipped' => __('Unshipped', 'fluent-cart'),
264 'shipped' => __('Shipped', 'fluent-cart'),
265 'delivered' => __('Delivered', 'fluent-cart'),
266 'unshippable' => __('Unshippable', 'fluent-cart'),
267 ], []
268 ], '1.3.16', 'fluent_cart/shipping_statuses', 'Use fluent_cart/shipping_statuses instead of fluent-cart/shipping_statuses.');
269
270 return apply_filters('fluent_cart/shipping_statuses', $statuses, []);
271 }
272
273 public static function getEditableShippingStatuses()
274 {
275 $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
276 [
277 'unshipped' => __('Unshipped', 'fluent-cart'),
278 'shipped' => __('Shipped', 'fluent-cart'),
279 'delivered' => __('Delivered', 'fluent-cart'),
280 'unshippable' => __('Unshippable', 'fluent-cart'),
281 ], []
282 ], '1.3.16', 'fluent_cart/editable_shipping_statuses', 'Use fluent_cart/editable_shipping_statuses instead of fluent-cart/editable_order_statuses.');
283
284 return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []);
285 }
286
287 public static function getOrderSuccessStatuses()
288 {
289 return [
290 'completed',
291 // 'archived',
292 'processing',
293 ];
294 }
295
296 public static function getOrderFailedStatuses()
297 {
298 return [
299 'failed',
300 //'refunded',
301 'cancelled',
302 ];
303 }
304
305 public static function getTransactionSuccessStatuses()
306 {
307 return [
308 'paid',
309 ];
310 }
311
312 public static function getTransactionStatuses($withLabel = true)
313 {
314 $statuses = apply_filters_deprecated('fluent-cart/transaction_statuses', [
315 [
316 'pending' => __('Pending', 'fluent-cart'),
317 'paid' => __('Paid', 'fluent-cart'),
318 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'),
319 'failed' => __('Failed', 'fluent-cart'),
320 'refunded' => __('Refunded', 'fluent-cart'),
321 'active' => __('Active', 'fluent-cart'),
322 ], []
323 ], '1.3.16', 'fluent_cart/transaction_statuses', 'Use fluent_cart/transaction_statuses instead of fluent-cart/transaction_statuses.');
324
325 $statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []);
326
327 if ($withLabel) {
328 return $statuses;
329 }
330
331 return array_keys($statuses);
332 }
333
334 public static function getEditableTransactionStatuses($withLabel = true)
335 {
336 $statuses = apply_filters_deprecated('fluent-cart/editable_transaction_statuses', [
337 [
338 'pending' => __('Pending', 'fluent-cart'),
339 'paid' => __('Paid', 'fluent-cart'),
340 'failed' => __('Failed', 'fluent-cart'),
341 'refunded' => __('Refunded', 'fluent-cart'),
342 ], []
343 ], '1.3.16', 'fluent_cart/editable_transaction_statuses', 'Use fluent_cart/editable_transaction_statuses instead of fluent-cart/editable_transaction_statuses.');
344
345 $statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []);
346
347 if ($withLabel) {
348 return $statuses;
349 }
350
351 return array_keys($statuses);
352 }
353
354 public static function loadSpoutLib()
355 {
356 static $loaded;
357
358 if ($loaded) {
359 return $loaded;
360 }
361
362 require_once FLUENTCART_PLUGIN_PATH . 'app/Services/Libs/Spout/Autoloader/autoload.php';
363
364 return true;
365 }
366
367 public static function productStatuses($withLabel = true): array
368 {
369 $statues = [
370 'publish' => __('Publish', 'fluent-cart'),
371 'draft' => __('Draft', 'fluent-cart'),
372 'future' => __('Scheduled', 'fluent-cart'),
373 'private' => __('Private', 'fluent-cart'),
374 'trash' => __('Trashed', 'fluent-cart'),
375 ];
376
377 if ($withLabel) {
378 return $statues;
379 }
380
381 return array_keys($statues);
382 }
383
384 public static function productAdminAllStatuses()
385 {
386 $statuses = self::productStatuses();
387 unset($statuses['trash']);
388 return array_keys($statuses);
389 }
390
391 public static function getCartDriver()
392 {
393 return 'db';
394 }
395
396 /**
397 * Convert an amount to a formatted decimal string.
398 *
399 * @param float $amount The amount to convert. (required)
400 * @param bool $withCurrency Whether to include the currency symbol. (optional, default: true)
401 * @param string|null $currencyCode The currency code to use. (optional, default: null)
402 * @param bool $formatted Whether to format the amount. (optional, default: true)
403 * @param bool $showDecimals Whether to show decimal places. (optional, default: true)
404 * @param bool $thousand_separator Whether to include thousand separators. (optional, default: true)
405 *
406 * @return string The formatted amount.
407 *
408 * @dev Note: If you don't want the thousand separator to be applied,
409 * you need to set $formatted to true and $thousand_separator to false.
410 *
411 * @hook fluent_cart/hide_unnecessary_decimals - Filter to control whether unnecessary decimals (like .00) should be hidden.
412 * Usage: add_filter('fluent_cart/hide_unnecessary_decimals', '__return_true'); // This will show 10 instead of 10.00
413 */
414 public static function toDecimal($amount, $withCurrency = true, $currencyCode = null, $formatted = true, $showDecimals = true, $thousand_separator = true, $withTranslatedDigit = true)
415 {
416 if (!is_numeric($amount)) {
417 return $amount;
418 }
419
420 // Set default decimal places to 2
421 $decimal = 2;
422
423 // Check if the shop is using a zero-decimal currency
424 if (self::shopConfig('is_zero_decimal')) {
425 $decimal = 0;
426 }
427
428 // Use provided or default currency code
429 if (!$currencyCode) {
430 $currencyCode = self::shopConfig('currency');
431 }
432
433 // Get currency sign
434 $sign = CurrenciesHelper::getCurrencySign($currencyCode);
435
436 $amount = floatVal($amount / 100);
437
438 // If $showDecimals is false, we override decimal places to 0
439 if (!$showDecimals) {
440 $decimal = 0;
441 }
442
443 // Format the amount based on the decimal configuration
444 if ($formatted) {
445 $decimal_separator = self::shopConfig('decimal_separator') === 'comma' ? ',' : '.';
446
447 $thousand_separator = $decimal_separator === ',' ? '.' : ',';
448
449 // Check if we should hide unnecessary decimal places (e.g., 10.00 -> 10)
450 $hideUnnecessaryDecimals = apply_filters('fluent_cart/hide_unnecessary_decimals', false, [
451 'amount' => $amount,
452 'decimal' => $decimal
453 ]);
454
455 $amount = number_format(
456 $amount,
457 $decimal,
458 $decimal_separator,
459 $thousand_separator
460 );
461
462 if ($hideUnnecessaryDecimals && $decimal > 0) {
463 // Remove trailing zeros only from the decimal portion
464 $parts = explode($decimal_separator, $amount);
465 if (count($parts) === 2) {
466 $parts[1] = rtrim($parts[1], '0');
467 if ($parts[1] === '') {
468 $amount = $parts[0];
469 } else {
470 $amount = $parts[0] . $decimal_separator . $parts[1];
471 }
472 }
473 }
474
475 if ($withTranslatedDigit) {
476 $amount = self::translateNumber($amount);
477 }
478 }
479
480 // If $withCurrency is false, just return the formatted amount
481 if (!$withCurrency) {
482 return $amount;
483 }
484
485 // Get currency position and return formatted amount with currency
486 $position = self::shopConfig('currency_position');
487
488 switch ($position) {
489 case 'before':
490 return $sign . $amount;
491 case 'after':
492 return $amount . $sign;
493 case 'iso_before':
494 return $currencyCode . ' ' . $amount;
495 case 'iso_after':
496 return $amount . ' ' . $currencyCode;
497 case 'symbool_before_iso':
498 return $sign . $amount . ' ' . $currencyCode;
499 case 'symbool_after_iso':
500 return $currencyCode . ' ' . $amount . $sign;
501 case 'symbool_and_iso':
502 return $currencyCode . ' ' . $sign . $amount;
503 default:
504 return $sign . $amount;
505 }
506 }
507
508 public static function toCent($amount): int
509 {
510 if (!is_numeric($amount)) {
511 return 0;
512 }
513
514 $amount = floatval($amount) * 100; // Convert to float and multiply
515 $amount = (int)round($amount); // Round to nearest integer, then cast
516 return $amount;
517 }
518
519 public static function toDecimalWithoutComma($amount)
520 {
521
522 if (!is_numeric($amount)) {
523 return 0;
524 }
525
526 // Convert to float and divide by 100
527 $result = floatval($amount) / 100;
528
529 // Ensure exactly two decimal places
530 return round($result, 2);
531 }
532
533 public static function getCustomerByUser($user)
534 {
535 if (is_numeric($user)) {
536 $user = get_user_by('ID', $user);
537 }
538
539 if (!$user) {
540 return null;
541 }
542
543 return Customer::query()->where('user_id', $user->ID)
544 ->orWhere('email', $user->user_email)
545 ->first();
546 }
547
548 /**
549 * @param array $order_data
550 *
551 * @return array return ['billing_address','shipping_address','others'];
552 */
553
554 /**
555 *
556 * @return string
557 */
558 public static function getProductImageBaseUri(): string
559 {
560 $uploads = wp_upload_dir();
561
562 return $uploads['baseurl'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
563 }
564
565 public static function getProductImageBaseDir()
566 {
567 $uploads = wp_upload_dir();
568
569 return $uploads['basedir'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
570 }
571
572 public static function getAvailableCurrencyList()
573 {
574 $currencies = apply_filters_deprecated('fluent-cart/available_currencies', [
575 [
576 'BDT' => [
577 "label" => __('Bangladeshi Taka', 'fluent-cart'),
578 "value" => 'BDT',
579 "symbol" => '৳',
580 ],
581 'USD' => [
582 "label" => __('United State Dollar', 'fluent-cart'),
583 "value" => 'USD',
584 "symbol" => '$',
585 ],
586 'GBP' => [
587 "label" => __('United Kingdom', 'fluent-cart'),
588 "value" => 'GBP',
589 "symbol" => '£',
590 ],
591 ], []
592 ], '1.3.16', 'fluent_cart/available_currencies', 'Use fluent_cart/available_currencies instead of fluent-cart/available_currencies.');
593
594 return apply_filters('fluent_cart/available_currencies', $currencies, []);
595 }
596
597 public static function getSymbolForCurrency($currency = 'BDT')
598 {
599
600 $symbol = '৳';
601 $list = self::getAvailableCurrencyList();
602
603 return $list[$currency]['symbol'] ?? $symbol;
604 }
605
606 public function getConfirmationSettings()
607 {
608 return (new Confirmation())->get();
609 }
610
611
612 /**
613 *
614 * @param $remove
615 * @return string
616 */
617 public static function getCheckoutPageLinkAfterRemovingGetParams($remove = [])
618 {
619
620 global $fct_store;
621
622 $link = Arr::get($fct_store, 'checkout_link');
623
624 $link .= '?';
625
626 if (!empty($remove)) {
627 $params = App::request()->all();
628
629 foreach ($params as $key => $val) {
630
631 if (!in_array($key, $remove)) {
632
633 $link .= $key . '=' . $val . '&';
634 }
635 }
636 }
637
638 return rtrim($link, '&');
639 }
640
641
642 /**
643 *
644 * @return bool
645 */
646 public static function isSingleProductPage(): bool
647 {
648 return is_singular([FluentProducts::CPT_NAME]);
649 }
650
651
652 public static function isTrue($array, $key)
653 {
654 $value = $array[$key] ?? false;
655 if (is_bool($value)) {
656 return $value;
657 }
658 if ($value === 'false' || !$value) {
659 return false;
660 }
661 return true;
662 }
663
664 public static function is_valid_json($string): bool
665 {
666 if (!is_string($string)) {
667 return false;
668 }
669
670 $trimmed = trim($string);
671
672 // Basic check: must start with { or [ and end with } or ]
673 if (!preg_match('/^(\{.*\}|\[.*\])$/s', $trimmed)) {
674 return false;
675 }
676
677 json_decode($trimmed);
678 return json_last_error() === JSON_ERROR_NONE;
679 }
680
681 public static function getStockStatuses($withLabel = true)
682 {
683 $statues = [
684 'in-stock' => __('In Stock', 'fluent-cart'),
685 'out-of-stock' => __('Out Of Stock', 'fluent-cart'),
686 ];
687
688 if ($withLabel) {
689 return $statues;
690 }
691
692 return array_keys($statues);
693 }
694
695 public static function getFulfilmentTypes($withLabel = true)
696 {
697 $statues = [
698 'physical' => __('Physical', 'fluent-cart'),
699 'digital' => __('Digital', 'fluent-cart'),
700 ];
701
702 if ($withLabel) {
703 return $statues;
704 }
705
706 return array_keys($statues);
707 }
708
709 public static function getVariationTypes($withLabel = true)
710 {
711 $statues = [
712 'simple' => __('Simple', 'fluent-cart'),
713 'simple_variations' => __('Simple Variation', 'fluent-cart'),
714 ];
715
716 if ($withLabel) {
717 return $statues;
718 }
719
720 return array_keys($statues);
721 }
722
723 public static function isValueEncrypted($raw_value)
724 {
725 if (!$raw_value || !is_string($raw_value) || !extension_loaded('openssl')) {
726 return false;
727 }
728
729 // Check if input is valid base64
730 if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $raw_value)) {
731 return false;
732 }
733
734 $decoded = base64_decode($raw_value, true);
735 if ($decoded === false) {
736 return false;
737 }
738
739 // Check if decoded string is long enough for IV
740 $method = 'aes-256-ctr';
741 $ivlen = openssl_cipher_iv_length($method);
742 if (strlen($decoded) < $ivlen) {
743 return false;
744 }
745
746 $iv = substr($decoded, 0, $ivlen);
747 $ciphertext = substr($decoded, $ivlen);
748
749 $key = (defined('FLUENT_CART_ENCRYPTION_KEY'))
750 ? FLUENT_CART_ENCRYPTION_KEY
751 : ((defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY)
752 ? LOGGED_IN_KEY
753 : 'this-is-a-fallback-key-but-not-secure');
754 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT)
755 ? LOGGED_IN_SALT
756 : 'this-is-a-fallback-salt-but-not-secure';
757
758 $value = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
759 if ($value === false) {
760 return false;
761 }
762
763
764 return substr($value, -strlen($salt)) === $salt;
765 }
766
767 public static function encryptKey($value)
768 {
769 if (!$value) {
770 return $value;
771 }
772
773 if (!extension_loaded('openssl')) {
774 return $value;
775 }
776
777 if (self::isValueEncrypted($value)) {
778 return $value;
779 }
780
781 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
782
783 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
784 $key = FLUENT_CART_ENCRYPTION_KEY;
785 } else {
786 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
787 }
788
789 $method = 'aes-256-ctr';
790 $ivlen = openssl_cipher_iv_length($method);
791 $iv = openssl_random_pseudo_bytes($ivlen);
792
793 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
794 if (!$raw_value) {
795 return false;
796 }
797
798 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
799 }
800
801 public static function decryptKey($raw_value)
802 {
803
804 if (!$raw_value) {
805 return $raw_value;
806 }
807
808 if (!extension_loaded('openssl')) {
809 return $raw_value;
810 }
811
812 $decoded = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
813
814 if ($decoded === false) {
815 return $raw_value;
816 }
817
818 $method = 'aes-256-ctr';
819 $ivlen = openssl_cipher_iv_length($method);
820
821 if (strlen($decoded) <= $ivlen) {
822 return $raw_value;
823 }
824
825 $iv = substr($decoded, 0, $ivlen);
826
827 $ciphertext = substr($decoded, $ivlen);
828
829 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
830 $key = FLUENT_CART_ENCRYPTION_KEY;
831 } else {
832 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
833 }
834
835 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
836
837 $decrypted = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
838 if (!$decrypted || substr($decrypted, -strlen($salt)) !== $salt) {
839 return false;
840 }
841
842 return substr($decrypted, 0, -strlen($salt));
843 }
844
845 /**
846 * @return array
847 * For kses_post to allow some html tags
848 */
849 public static function allowedHTMLForCheckout(): array
850 {
851
852 $allowedTags = [
853 'svg' => [
854 'class' => true,
855 'aria-hidden' => true,
856 'aria-labelledby' => true,
857 'role' => true,
858 'xmlns' => true,
859 'width' => true,
860 'height' => true,
861 'viewbox' => true,
862 'fill' => true
863 ],
864 'g' => [
865 'fill' => true
866 ],
867 'title' => ['title' => true],
868 'path' => [
869 'd' => true,
870 'fill' => true,
871 'stroke' => true,
872 'fill-rule' => true,
873 'clip-rule' => true,
874 ],
875 ];
876
877 foreach (['input', 'label', 'div', 'span', 'p', 'select', 'option', 'textarea', 'button', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $tag) {
878 $allowedTags[$tag] = [
879 'selected',
880 'type' => [],
881 "selected='selected'" => [],
882 'name' => [],
883 'value' => [],
884 'autocomplete' => [],
885 'placeholder' => [],
886 'data-required' => [],
887 'data-type' => [],
888 'id' => [],
889 'class' => [],
890 'required' => [],
891 'disabled' => [],
892 'for' => [],
893 'style' => [],
894 'checked' => [],
895 'maxlength' => [],
896 'data-id' => [],
897 'data-country' => [],
898 'data-fluent-cart-checkout-page-form-address-modal-open-button' => [],
899 'data-fluent-cart-checkout-page-form-address-modal-wrapper' => [],
900 'data-fluent-cart-checkout-page-form-input-wrapper' => [],
901 'data-fluent_cart_checkout_error' => [],
902 'data-fluent-cart-checkout-page-form-address-modal-body' => [],
903 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button' => [],
904 'data-fluent-cart-checkout-page-form-address-input' => [],
905 'data-fluent-cart-checkout-page-form-address-select-wrapper' => [],
906 'data-fluent-cart-checkout-page-form-address-modal-close-button' => [],
907 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper' => [],
908 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-button' => [],
909 'data-fluent-cart-checkout-page-form-address-modal-apply-button' => [],
910 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-form-wrapper' => [],
911 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-submit-button' => [],
912 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-cancel-button' => [],
913 'data-fluent-cart-address-type' => [],
914 'data-fluent-cart-checkout-page-form-address-info-wrapper' => [],
915 'data-fluent-cart-checkout-page-form-error' => [],
916 'data-fluent-cart-checkout-page-form-section' => [],
917 'data-fluent-cart-checkout-page-discount-container' => [],
918 'data-fluent-cart-checkout-page-final-amount-container' => [],
919 'data-fluent-cart-checkout-page-new-total-amount' => [],
920 'data-fluent-cart-checkout-page-final-amount' => [],
921 'data-fluent-cart-checkout-page-coupon-validate' => [],
922 'data-fluent-cart-checkout-coupon-items-toggle' => [],
923 'data-fluent-cart-checkout-coupon-items-wrapper' => []
924
925
926 ];
927 }
928
929 return $allowedTags;
930 }
931
932 public static function getCouponStatuses()
933 {
934 $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [
935 [
936 'active' => __('Active', 'fluent-cart'),
937 'expired' => __('Expired', 'fluent-cart'),
938 'disabled' => __('Disabled', 'fluent-cart'),
939 ], []
940 ], '1.3.16', 'fluent_cart/coupon_statuses', 'Use fluent_cart/coupon_statuses instead of fluent-cart/coupon_statuses.');
941
942 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
943 }
944
945 public static function getCouponSuccessStatuses()
946 {
947 return [
948 'active'
949 ];
950 }
951
952
953 /**
954 * Compact subscription terms as text
955 *
956 * Expected $data keys:
957 * - trial_days (int)
958 * - interval ('daily'|'weekly'|'monthly'|'yearly')
959 * - interval_count (int, default 1)
960 * - times (int; 0 = open-ended, >0 = finite number of payments/cycles)
961 * - price (string; formatted with currency)
962 * - signup_fee (string; 0 if none)
963 * - compare_price (string; 0 if none)
964 *
965 * Output examples:
966 * - "30 days free then $100.00 per year"
967 * - "$100 per year + $10 one-time signup fee"
968 * - "$100/month for 4 months"
969 * - "30 days free then $100/month for 4 months"
970 * - "$99 per month"
971 */
972 public static function getSubscriptionTermText(array $data, $asHtml = false): string
973 {
974 // Fixed interval options
975 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
976
977 // Normalize / defaults
978 $trialDays = $data['trial_days'] ?? 0;
979 $interval = (string)($data['interval'] ? $data['interval'] : 'monthly');
980
981 $unit = '';
982 if (isset($intervalOptions[$interval])) {
983 $unit = $intervalOptions[$interval];
984 } else if ($interval) {
985 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
986 foreach ($intervalOptions as $option) {
987 if ($option['value'] === $interval) {
988 $unit = strtolower($option['label']);
989 break;
990 }
991 }
992
993 if (!$unit) {
994 $unit = strtolower(str_replace(['_', '-'], ' ', $interval));
995 }
996 }
997
998 if (!$unit) {
999 $unit = 'year';
1000 }
1001
1002 $count = max(1, (int)($data['interval_count'] ?? 1));
1003 $times = (int)($data['times'] ?? 0);
1004 $price = (string)($data['price'] ?? '');
1005 $signupFee = $data['signup_fee'] ?? '';
1006 $compare = $data['compare_price'] ?? '';
1007
1008 $signupFeeLabel = $data['signup_fee_label'] ?? __('signup fee', 'fluent-cart');
1009
1010 // helpers
1011 $pluralUnit = static function (string $unit, int $n): string {
1012 switch ($unit) {
1013 case 'day':
1014 return _n('day', 'days', $n, 'fluent-cart');
1015 case 'week':
1016 return _n('week', 'weeks', $n, 'fluent-cart');
1017 case 'quarter':
1018 return _n('quarter', 'quarters', $n, 'fluent-cart');
1019 case 'half_year':
1020 return _n('six month', 'six months', $n, 'fluent-cart');
1021 case 'year':
1022 return _n('year', 'years', $n, 'fluent-cart');
1023 case 'month':
1024 return _n('month', 'months', $n, 'fluent-cart');
1025 default:
1026 // For custom intervals, add 's' for plural
1027 return ($n > 1) ? $unit . 's' : $unit;
1028 }
1029 };
1030
1031 // Build the “per …” phrase (e.g., "per month", "per 3 months")
1032 $perPhrase = ($count === 1)
1033 ? sprintf(
1034 /* translators: %s is the singular unit name (e.g., day, month, year) */
1035 __('per %s', 'fluent-cart'),
1036 $pluralUnit($unit, 1)
1037 )
1038 : sprintf(
1039 /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
1040 __('per %1$d %2$s', 'fluent-cart'),
1041 $count,
1042 $pluralUnit($unit, $count)
1043 );
1044
1045 // Main price phrase:
1046 // - If monthly installments (times > 0 and unit == month and count == 1): "$X/month for N months"
1047 // - Else if times > 0: "$X per {count unit(s)} for N {cycle(s)}"
1048 // - Else: "$X per {count unit(s)}"
1049 if ($times > 0) {
1050 if ($unit === 'month' && $count === 1) {
1051 $installmentTail = sprintf(
1052 /* translators: %s: pluralized 'month(s)' */
1053 __('for %1$d %2$s', 'fluent-cart'),
1054 $times,
1055 $pluralUnit('month', $times)
1056 );
1057 $main = sprintf(
1058 /* translators: Compact monthly installment, e.g. "$100/month for 4 months" */
1059 __('%1$s/%2$s %3$s', 'fluent-cart'),
1060 $price,
1061 __('month', 'fluent-cart'),
1062 $installmentTail
1063 );
1064 } else {
1065 // Generic installment wording: "for N cycle(s)"
1066 $cyclesTail = sprintf(
1067 /* translators: %1$d is the number of cycles, %2$s is "cycle" or "cycles" */
1068 __('for %1$d %2$s', 'fluent-cart'),
1069 $times,
1070 _n('cycle', 'cycles', $times, 'fluent-cart')
1071 );
1072 $main = sprintf(
1073 /* translators: e.g. "$100 per 3 months for 4 cycles" */
1074 __('%1$s %2$s %3$s', 'fluent-cart'),
1075 $price,
1076 $perPhrase,
1077 $cyclesTail
1078 );
1079 }
1080 } else {
1081 // Open-ended subscription
1082 $main = sprintf(
1083 /* translators: e.g. "$99 per month" */
1084 __('%1$s %2$s', 'fluent-cart'),
1085 $price,
1086 $perPhrase
1087 );
1088 }
1089
1090 // Prefix trial if present: "N days free then …"
1091 if ($trialDays > 0) {
1092 $trialFrag = sprintf(
1093 /* translators: "30 days free then" */
1094 __('%1$d %2$s free then', 'fluent-cart'),
1095 $trialDays,
1096 $pluralUnit('day', $trialDays)
1097 );
1098 $main = $trialFrag . ' ' . $main;
1099 }
1100
1101 // Append signup fee if any: " + $10 one-time signup fee"
1102 if ($signupFee) {
1103 $main .= ' ' . sprintf(
1104 /* translators: e.g. "+ $10 one-time signup fee" */
1105 __('+ %1$s one-time %2$s', 'fluent-cart'),
1106 $signupFee,
1107 $signupFeeLabel
1108 );
1109 }
1110
1111 // No trailing period (matches your examples)
1112 return $main;
1113 }
1114
1115 public static function getTranslatedIntervalUnit(string $unit): string
1116 {
1117 switch ($unit) {
1118 case 'day':
1119 return __('day', 'fluent-cart');
1120 case 'week':
1121 return __('week', 'fluent-cart');
1122 case 'month':
1123 return __('month', 'fluent-cart');
1124 case 'quarter':
1125 return __('quarter', 'fluent-cart');
1126 case 'half_year':
1127 return __('six month', 'fluent-cart');
1128 case 'year':
1129 return __('year', 'fluent-cart');
1130 default:
1131 return $unit;
1132 }
1133 }
1134
1135 public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string
1136 {
1137 // Convert to array only if it's an object
1138 if (is_object($otherInfo)) {
1139 $otherInfo = json_decode(json_encode($otherInfo), true);
1140 }
1141
1142 $price = self::toDecimal($itemPrice, true, $currencyCode);
1143 $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
1144
1145 if ($recurringDiscountAmount) {
1146 $newRecurringAmount = $itemPrice - $recurringDiscountAmount;
1147 $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode);
1148 }
1149
1150 $repeatInterval = Arr::get($otherInfo, 'repeat_interval', '');
1151 $occurrence = (int)Arr::get($otherInfo, 'times', 0);
1152
1153 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1154
1155 $intervalUnit = '';
1156 if (isset($intervalOptions[$repeatInterval])) {
1157 $intervalUnit = $intervalOptions[$repeatInterval];
1158 } else if ($repeatInterval) {
1159 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1160 foreach ($intervalOptions as $option) {
1161 if ($option['value'] === $repeatInterval) {
1162 $intervalUnit = strtolower($option['label']);
1163 break;
1164 }
1165 }
1166
1167 if (!$intervalUnit) {
1168 $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval));
1169 }
1170 }
1171
1172 $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit);
1173
1174 $interval = $intervalUnit
1175 ? sprintf(
1176 /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */
1177 __('per %s', 'fluent-cart'),
1178 $intervalLabel
1179 )
1180 : '';
1181
1182 $time = $intervalUnit
1183 ? $intervalLabel
1184 : '';
1185
1186 $paymentInfo = sprintf(
1187 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text, %4$s is the occurrence count, %5$s is the time period */
1188 __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
1189 $price,
1190 $interval,
1191 $occurrence,
1192 $time
1193 );
1194
1195 if (empty($occurrence)) {
1196 $paymentInfo = sprintf(
1197 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
1198 __('%1$s %2$s %3$s', 'fluent-cart'),
1199 $price,
1200 $interval,
1201 __('until cancel', 'fluent-cart')
1202 );
1203 }
1204
1205 return !empty($otherInfo) ? $paymentInfo : null;
1206 }
1207
1208 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1209 {
1210 // Convert to array if it's an object
1211 if (is_object($otherInfo)) {
1212 $otherInfo = json_decode(json_encode($otherInfo), true);
1213 }
1214
1215 $signupFeeName = Arr::get($otherInfo, 'signup_fee_name', __('Setup Fee', 'fluent-cart'));
1216 $fee = Arr::get($otherInfo, 'signup_fee', 0);
1217 $manageSetupFee = Arr::get($otherInfo, 'manage_setup_fee', 'no');
1218
1219 if ($manageSetupFee !== 'yes' || !$fee) {
1220 return '';
1221 }
1222
1223
1224 if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) {
1225 if ($fee != $originalSetupFee) {
1226 $title = __('Adjusted setup fee', 'fluent-cart');
1227 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1228
1229 if ($asArray) {
1230 return [
1231 'signup_fee_name' => $title,
1232 'signup_fee' => $fee,
1233 'signup_fee_formatted' => $formattedAmount,
1234 ];
1235 }
1236 return $title . $formattedAmount;
1237 }
1238 }
1239
1240 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1241 if ($asArray) {
1242 return [
1243 'signup_fee_name' => $signupFeeName,
1244 'signup_fee' => $fee,
1245 'signup_fee_formatted' => $formattedAmount,
1246 ];
1247 }
1248
1249
1250 return $signupFeeName . ' ' . $formattedAmount;
1251 }
1252
1253 public static function generateTrialInfo($otherInfo)
1254 {
1255 $trialInfo = '';
1256
1257 $trialDays = Arr::get($otherInfo, 'trial_days', 0);
1258
1259 if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
1260 $trialInfo = sprintf(
1261 /* translators: %d is the number of trial days */
1262 __('Free Trial: %d days', 'fluent-cart'),
1263 $trialDays
1264 );
1265 }
1266
1267 return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
1268 }
1269
1270
1271 public static function getCountryList(): array
1272 {
1273 $options = App::getInstance('localization')->countriesOptions();
1274 $options = apply_filters_deprecated('fluent-cart/util/countries', [$options, []], '1.3.16', 'fluent_cart/util/countries', 'Use fluent_cart/util/countries instead of fluent-cart/util/countries.');
1275
1276 return apply_filters('fluent_cart/util/countries', $options, []);
1277 }
1278
1279 public static function getCountyIsoLists(): array
1280 {
1281 return App::getInstance('localization')->getCountyIsoLists();
1282 }
1283
1284 public static function getCountryCode($country_name)
1285 {
1286 $countries = self::getCountryList();
1287 foreach ($countries as $country) {
1288 if ($country['name'] === $country_name) {
1289 return $country['value'];
1290 }
1291 }
1292 return '';
1293 }
1294
1295 /**
1296 * Get the country's name with country code,
1297 *
1298 * @param $code
1299 * @return string
1300 */
1301 public static function getCountryName($code): string
1302 {
1303 if (!$code || !is_string($code)) {
1304 return '';
1305 }
1306
1307 $countries = self::getCountyIsoLists();
1308
1309 return $countries[$code] ?? $code;
1310 }
1311
1312 public static function languageCodes(): array
1313 {
1314 return $langCodes = [
1315 'AF' => 'fa-AF',
1316 'AL' => 'sq-AL',
1317 'DZ' => 'ar-DZ',
1318 'AS' => 'sm-AS',
1319 'AD' => 'ca-AD',
1320 'AO' => 'pt-AO',
1321 'AR' => 'es-AR',
1322 'AM' => 'hy-AM',
1323 'AU' => 'en-AU',
1324 'AT' => 'de-AT',
1325 'AZ' => 'az-AZ',
1326 'BH' => 'ar-BH',
1327 'BD' => 'bn-BD',
1328 'BY' => 'be-BY',
1329 'BE' => 'nl-BE',
1330 'BZ' => 'en-BZ',
1331 'BJ' => 'fr-BJ',
1332 'BT' => 'dz-BT',
1333 'BO' => 'es-BO',
1334 'BA' => 'bs-BA',
1335 'BW' => 'en-BW',
1336 'BR' => 'pt-BR',
1337 'BN' => 'ms-BN',
1338 'BG' => 'bg-BG',
1339 'BF' => 'fr-BF',
1340 'BI' => 'fr-BI',
1341 'KH' => 'km-KH',
1342 'CM' => 'en-CM',
1343 'CA' => 'en-CA',
1344 'CV' => 'pt-CV',
1345 'CF' => 'fr-CF',
1346 'TD' => 'fr-TD',
1347 'CL' => 'es-CL',
1348 'CN' => 'zh-CN',
1349 'CO' => 'es-CO',
1350 'KM' => 'ar-KM',
1351 'CD' => 'fr-CD',
1352 'CG' => 'fr-CG',
1353 'CR' => 'es-CR',
1354 'CI' => 'fr-CI',
1355 'HR' => 'hr-HR',
1356 'CU' => 'es-CU',
1357 'CY' => 'el-CY',
1358 'CZ' => 'cs-CZ',
1359 'DK' => 'da-DK',
1360 'DJ' => 'fr-DJ',
1361 'DM' => 'en-DM',
1362 'DO' => 'es-DO',
1363 'EC' => 'es-EC',
1364 'EG' => 'ar-EG',
1365 'SV' => 'es-SV',
1366 'GQ' => 'es-GQ',
1367 'ER' => 'ti-ER',
1368 'EE' => 'et-EE',
1369 'ET' => 'am-ET',
1370 'FJ' => 'en-FJ',
1371 'FI' => 'fi-FI',
1372 'FR' => 'fr-FR',
1373 'GA' => 'fr-GA',
1374 'GM' => 'en-GM',
1375 'GE' => 'ka-GE',
1376 'DE' => 'de-DE',
1377 'GH' => 'en-GH',
1378 'GR' => 'el-GR',
1379 'GD' => 'en-GD',
1380 'GT' => 'es-GT',
1381 'GN' => 'fr-GN',
1382 'GW' => 'pt-GW',
1383 'GY' => 'en-GY',
1384 'HT' => 'fr-HT',
1385 'HN' => 'es-HN',
1386 'HU' => 'hu-HU',
1387 'IS' => 'is-IS',
1388 'IN' => 'hi-IN',
1389 'ID' => 'id-ID',
1390 'IR' => 'fa-IR',
1391 'IQ' => 'ar-IQ',
1392 'IE' => 'en-IE',
1393 'IL' => 'he-IL',
1394 'IT' => 'it-IT',
1395 'JM' => 'en-JM',
1396 'JP' => 'ja-JP',
1397 'JO' => 'ar-JO',
1398 'KZ' => 'kk-KZ',
1399 'KE' => 'sw-KE',
1400 'KI' => 'en-KI',
1401 'KR' => 'ko-KR',
1402 'KW' => 'ar-KW',
1403 'KG' => 'ky-KG',
1404 'LA' => 'lo-LA',
1405 'LV' => 'lv-LV',
1406 'LB' => 'ar-LB',
1407 'LS' => 'en-LS',
1408 'LR' => 'en-LR',
1409 'LY' => 'ar-LY',
1410 'LI' => 'de-LI',
1411 'LT' => 'lt-LT',
1412 'LU' => 'lb-LU',
1413 'MG' => 'mg-MG',
1414 'MW' => 'en-MW',
1415 'MY' => 'ms-MY',
1416 'MV' => 'dv-MV',
1417 'ML' => 'fr-ML',
1418 'MT' => 'mt-MT',
1419 'MH' => 'mh-MH',
1420 'MR' => 'ar-MR',
1421 'MU' => 'mfe-MU',
1422 'MX' => 'es-MX',
1423 'FM' => 'en-FM',
1424 'MD' => 'ro-MD',
1425 'MC' => 'fr-MC',
1426 'MN' => 'mn-MN',
1427 'ME' => 'sr-ME',
1428 'MA' => 'ar-MA',
1429 'MZ' => 'pt-MZ',
1430 'NA' => 'en-NA',
1431 'NR' => 'en-NR',
1432 'NP' => 'ne-NP',
1433 'NL' => 'nl-NL',
1434 'NZ' => 'en-NZ',
1435 'NI' => 'es-NI',
1436 'NG' => 'en-NG',
1437 'NO' => 'no-NO',
1438 'OM' => 'ar-OM',
1439 'PK' => 'ur-PK',
1440 'PW' => 'en-PW',
1441 'PA' => 'es-PA',
1442 'PG' => 'en-PG',
1443 'PY' => 'es-PY',
1444 'PE' => 'es-PE',
1445 'PH' => 'en-PH',
1446 'PL' => 'pl-PL',
1447 'PT' => 'pt-PT',
1448 'QA' => 'ar-QA',
1449 'RO' => 'ro-RO',
1450 'RU' => 'ru-RU',
1451 'RW' => 'rw-RW',
1452 'WS' => 'sm-WS',
1453 'SM' => 'it-SM',
1454 'SA' => 'ar-SA',
1455 'SN' => 'fr-SN',
1456 'RS' => 'sr-RS',
1457 'SC' => 'fr-SC',
1458 'SL' => 'en-SL',
1459 'SG' => 'en-SG',
1460 'SK' => 'sk-SK',
1461 'SI' => 'sl-SI',
1462 'SB' => 'en-SB',
1463 'SO' => 'so-SO',
1464 'ZA' => 'en-ZA',
1465 'ES' => 'es-ES',
1466 'LK' => 'si-LK',
1467 'SD' => 'ar-SD',
1468 'SR' => 'nl-SR',
1469 'SZ' => 'en-SZ',
1470 'SE' => 'sv-SE',
1471 'CH' => 'de-CH',
1472 'SY' => 'ar-SY',
1473 'TW' => 'zh-TW',
1474 'TJ' => 'tg-TJ',
1475 'TZ' => 'sw-TZ',
1476 'TH' => 'th-TH',
1477 'TL' => 'pt-TL',
1478 'TG' => 'fr-TG',
1479 'TO' => 'to-TO',
1480 'TT' => 'en-TT',
1481 'TN' => 'ar-TN',
1482 'TR' => 'tr-TR',
1483 'TM' => 'tk-TM',
1484 'TV' => 'en-TV',
1485 'UG' => 'en-UG',
1486 'UA' => 'uk-UA',
1487 'AE' => 'ar-AE',
1488 'GB' => 'en-GB',
1489 'US' => 'en-US',
1490 'UY' => 'es-UY',
1491 'UZ' => 'uz-UZ',
1492 'VU' => 'bi-VU',
1493 'VE' => 'es-VE',
1494 'VN' => 'vi-VN',
1495 'YE' => 'ar-YE',
1496 'ZM' => 'en-ZM',
1497 'ZW' => 'en-ZW'
1498 ];
1499 }
1500
1501 /**
1502 * Returns a translatable string with a shortcode inserted in the correct format.
1503 *
1504 * @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
1505 * @return string The formatted translatable string.
1506 */
1507 public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
1508 {
1509 $copyToClipboard = __('Copy to clipboard', 'fluent-cart');
1510 return sprintf(
1511 /* translators: %s: Shortcode */
1512 '<p>' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '</p>',
1513 '<code class="copyable-content" title="' . $copyToClipboard . '">' . ($shortcode) . '</code>',
1514 //$pageName
1515 );
1516 }
1517
1518
1519 /**
1520 * Get the current user Model.
1521 * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
1522 */
1523 public static function getCurrentUser($refresh = false)
1524 {
1525 static $user = false;
1526
1527 if (!$refresh && $user !== false) {
1528 return $user;
1529 }
1530
1531 $userId = get_current_user_id();
1532 if (!$userId) {
1533 $user = null;
1534 return $user;
1535 }
1536
1537 $user = User::query()->find($userId);
1538
1539 return $user;
1540
1541 }
1542
1543 public static function hasLicense($product): bool
1544 {
1545 if (empty($product)) {
1546 return false;
1547 }
1548
1549 $meta = Arr::get($product, 'licensesMeta.meta_value', []);
1550
1551 if (empty($meta)) {
1552 return false;
1553 }
1554
1555 $meta = is_string($meta) ? json_decode($meta, true) : $meta;
1556
1557 return Arr::get($meta, 'enabled') === 'yes';
1558
1559 }
1560
1561
1562 public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
1563 {
1564 $identifier = Arr::get($productDownload, 'download_identifier', '');
1565
1566 $validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
1567 'product_download' => $productDownload,
1568 'order_id' => $orderId,
1569 'is_admin' => $isAdmin,
1570 ]);
1571
1572 $signParams = [
1573 'download_identifier' => $identifier,
1574 'valid_till' => DateTime::now()
1575 ->addMinutes($validityInMinutes ?? 60)
1576 ->getTimestamp()
1577 ];
1578
1579 if ($orderId) {
1580 $orderId = Arr::wrap($orderId);
1581 $signParams['order_id'] = json_encode($orderId);
1582 }
1583
1584 $url = (new BaseUrl())->sign(site_url('/'), $signParams);
1585
1586 return URL::appendQueryParams($url, [
1587 'fluent-cart' => 'download-by-id',
1588 ]);
1589
1590 }
1591
1592
1593 public static function readableFileSize($bytes): string
1594 {
1595 // Converts bytes to a human-readable format (e.g., KB, MB, GB)
1596 // Example: 1024 -> "1 KB"
1597 // Example: 1048576 -> "1 MB"
1598
1599 if (!$bytes && $bytes !== 0) return '';
1600 $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1601 $i = floor(log($bytes, 2) / 10);
1602 $size = $bytes / pow(1024, $i);
1603 return round($size, 2) . ' ' . $units[$i];
1604 }
1605
1606
1607 public static function getSitePrefix()
1608 {
1609 $siteUrl = rtrim(home_url(), '/');
1610 // remove http:// or https:// from the URL
1611 $siteUrl = preg_replace('#^https?://#', '', $siteUrl);
1612 $sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
1613
1614 return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
1615 }
1616
1617 public static function humanIntervalMaps($interval = '')
1618 {
1619 $intervals = [
1620 'daily' => __('day', 'fluent-cart'),
1621 'weekly' => __('week', 'fluent-cart'),
1622 'monthly' => __('month', 'fluent-cart'),
1623 'quarterly' => __('quarter', 'fluent-cart'),
1624 'half_yearly' => __('six month', 'fluent-cart'),
1625 'yearly' => __('year', 'fluent-cart'),
1626 ];
1627
1628 return Arr::get($intervals, $interval);
1629 }
1630
1631 /**
1632 * @return array Array of intervals with label and value
1633 */
1634 public static function getAvailableSubscriptionIntervalOptions(): array
1635 {
1636 $intervals = [
1637 [
1638 'label' => __('Yearly', 'fluent-cart'),
1639 'value' => 'yearly',
1640 'map_value' => 'year',
1641 ],
1642 [
1643 'label' => __('Half Yearly', 'fluent-cart'),
1644 'value' => 'half_yearly',
1645 'map_value' => 'half_year',
1646 ],
1647 [
1648 'label' => __('Quarterly', 'fluent-cart'),
1649 'value' => 'quarterly',
1650 'map_value' => 'quarter',
1651 ],
1652 [
1653 'label' => __('Monthly', 'fluent-cart'),
1654 'value' => 'monthly',
1655 'map_value' => 'month',
1656 ],
1657 [
1658 'label' => __('Weekly', 'fluent-cart'),
1659 'value' => 'weekly',
1660 'map_value' => 'week',
1661 ],
1662 [
1663 'label' => __('Daily', 'fluent-cart'),
1664 'value' => 'daily',
1665 'map_value' => 'day',
1666 ]
1667 ];
1668
1669 return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
1670 }
1671
1672 public static function translateIntervalToStandardFormat($repeatInterval)
1673 {
1674 if (empty($repeatInterval)) {
1675 return 'year';
1676 }
1677
1678 $intervalMaps = static::getAvailableSubscriptionIntervalMaps();
1679
1680 if (!isset($intervalMaps[$repeatInterval])) {
1681 return 'year';
1682 }
1683
1684 return $intervalMaps[$repeatInterval];
1685 }
1686
1687 public static function getAvailableSubscriptionIntervalMaps()
1688 {
1689 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1690
1691 $intervalMaps = [];
1692 foreach ($intervalOptions as $option) {
1693 $intervalMaps[$option['value']] = $option['map_value'];
1694 }
1695
1696 return $intervalMaps;
1697
1698 }
1699
1700 public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
1701 {
1702 $intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
1703
1704 $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
1705 'existing_trial_days' => $trialDays,
1706 'repeat_interval' => $repeatInterval,
1707 'interval_in_days' => $intervalInDays,
1708 ]);
1709
1710 return min($trialDays + $intervalInDays, $maxTrialDaysAllowed); // return the minimum of the sum of the existing trial days and the interval days, and the max trial allowed
1711
1712 }
1713
1714 public static function subscriptionIntervalInDays($interval)
1715 {
1716 switch ($interval) {
1717 case 'daily':
1718 return 1;
1719 case 'weekly':
1720 return 7;
1721 case 'monthly':
1722 return 30;
1723 case 'quarterly':
1724 return 90;
1725 case 'half_yearly':
1726 return 182;
1727 case 'yearly':
1728 return 365;
1729 default:
1730 return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1731 'interval' => $interval,
1732 ]);
1733 }
1734 }
1735
1736 public static function parseTermIdsForFilter($filters): array
1737 {
1738 $taxonomies = Taxonomy::getTaxonomies();
1739 if (is_string($filters)) {
1740 $filters = json_decode($filters, true);
1741 }
1742 $formattedFilters = [];
1743
1744 foreach ($taxonomies as $key => $taxonomy) {
1745 $terms = Arr::get($filters, $key, []);
1746 if (!is_array($terms)) {
1747 $terms = [$terms];
1748 }
1749
1750 $terms = array_filter($terms, function ($term) {
1751 return !empty($term);
1752 });
1753
1754 if (!empty($terms)) {
1755 $terms = array_map(function ($term) {
1756 return sanitize_text_field((string)$term);
1757 }, $terms);
1758
1759
1760 $formattedFilters[$key] = $terms;
1761 }
1762
1763
1764 }
1765
1766 return $formattedFilters;
1767 }
1768
1769 public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
1770 {
1771 $result = [];
1772
1773 foreach ([$array1, $array2] as $array) {
1774 foreach ($array as $key => $values) {
1775 if (!isset($result[$key])) {
1776 $result[$key] = [];
1777 }
1778 $result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
1779 }
1780 }
1781
1782 return $result;
1783 }
1784
1785 public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
1786 {
1787 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1788
1789 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1790 $allChildVariants = Arr::except(
1791 $allChildVariants,
1792 Arr::pluck($variants, 'id')
1793 );
1794 $allChildVariants = array_filter($allChildVariants);
1795 $childVariants = ProductVariation::query()
1796 ->whereIn('id', $allChildVariants)
1797 ->with('product:ID,post_title')
1798 ->select($select)
1799 ->get()
1800 ->toArray();
1801
1802 // Extract post_title from product and remove product object to keep data clean
1803 foreach ($childVariants as $key => $childVariant) {
1804 $postTitle = Arr::get($childVariant, 'product.post_title');
1805 if ($postTitle) {
1806 $childVariants[$key]['post_title'] = $postTitle;
1807 unset($childVariants[$key]['product']);
1808 }
1809 }
1810
1811 foreach ($variants as &$variant) {
1812 $childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
1813 $variant['bundle_child_ids'] = $childIds;
1814 if (count($childIds) < 1) {
1815 $variant['child_variants'] = [];
1816 continue;
1817 }
1818 foreach ($childVariants as $childVariant) {
1819 if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
1820 $variant['child_variants'][$childVariant['id']] = $childVariant;
1821 }
1822 }
1823 }
1824
1825 return $variants;
1826 }
1827
1828 /**
1829 * Translate digits in a number according to the configured numeric system.
1830 *
1831 * Converts the digits 0-9 in the given number to their equivalents
1832 * defined in the numeric system string from `dateTimeStrings()`.
1833 * This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
1834 *
1835 * Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
1836 *
1837 * @param int|float|string $number The number to translate.
1838 * @return string The number with digits translated according to the configured numeric system.
1839 */
1840 public static function translateNumber($number): string
1841 {
1842 $config = TransStrings::dateTimeStrings();
1843 $numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
1844 $digits = explode('_', $numericSystem);
1845
1846 return strtr(
1847 (string)$number,
1848 array_combine(range(0, 9),
1849 $digits)
1850 );
1851 }
1852
1853 public static function isModalCheckoutEnabled(): bool
1854 {
1855 //$storeSettings = new StoreSettings();
1856 //$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
1857 return apply_filters('fluent_cart/enable_modal_checkout', false);
1858 }
1859
1860 public static function isAdminUser(): bool
1861 {
1862 return current_user_can('manage_options');
1863 }
1864
1865 /**
1866 * Convert string/boolean to actual boolean value.
1867 * Handles shortcode string attributes like "true"/"false"
1868 *
1869 * @param mixed $value The value to convert to boolean
1870 * @param bool $default Default value if conversion fails
1871 * @return bool The boolean result
1872 */
1873 public static function toBool($value, bool $default = false): bool
1874 {
1875 if (is_bool($value)) {
1876 return $value;
1877 }
1878
1879 if (is_string($value)) {
1880 $value = strtolower(trim($value));
1881 if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
1882 return true;
1883 }
1884 if (in_array($value, ['false', '0', 'no', 'off'], true)) {
1885 return false;
1886 }
1887 }
1888
1889 return $default;
1890 }
1891
1892 public static function formatTaxRatePercent(float $rate): string
1893 {
1894 $formatted = number_format($rate, 4, '.', '');
1895 if (strpos($formatted, '.') !== false) {
1896 $formatted = rtrim($formatted, '0');
1897 $formatted = rtrim($formatted, '.');
1898 }
1899 return $formatted;
1900 }
1901
1902 /**
1903 * Returns the tax label for order-level tax rows (tax_total, not per-item).
1904 * For mixed orders, indicates tax varies per item.
1905 *
1906 * @param \FluentCart\App\Models\Order $order
1907 * @return string
1908 */
1909 public static function getOrderTaxLabel($order) {
1910 if ((int) $order->tax_behavior === 3) {
1911 return esc_html__('(Varies)', 'fluent-cart');
1912 }
1913 return (int) $order->tax_behavior === 2
1914 ? esc_html__('(Included)', 'fluent-cart')
1915 : esc_html__('(Excluded)', 'fluent-cart');
1916 }
1917 }
1918