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

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

1,862 lines 60.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\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 !empty($packages) ? $packages[0] : 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, int $count = 1): string
1116 {
1117 switch ($unit) {
1118 case 'day':
1119 return _n('day', 'days', $count, 'fluent-cart');
1120 case 'week':
1121 return _n('week', 'weeks', $count, 'fluent-cart');
1122 case 'month':
1123 return _n('month', 'months', $count, 'fluent-cart');
1124 case 'quarter':
1125 return _n('quarter', 'quarters', $count, 'fluent-cart');
1126 case 'half_year':
1127 return _n('six month', 'six months', $count, 'fluent-cart');
1128 case 'year':
1129 return _n('year', 'years', $count, 'fluent-cart');
1130 default:
1131 return $count > 1 ? $unit . 's' : $unit;
1132 }
1133 }
1134
1135 public static function generateSubscriptionInfo($otherInfo, $itemPrice): ?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);
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);
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, $occurrence);
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): ?string
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 return __('Adjusted setup fee', 'fluent-cart') . CurrencySettings::getPriceHtml($fee, null, true, true);
1227 }
1228 }
1229
1230
1231 return $signupFeeName . ' ' . CurrencySettings::getPriceHtml($fee, null, true, true);
1232 }
1233
1234 public static function generateTrialInfo($otherInfo)
1235 {
1236 $trialInfo = '';
1237
1238 $trialDays = Arr::get($otherInfo, 'trial_days', 0);
1239
1240 if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
1241 $trialInfo = sprintf(
1242 /* translators: %d is the number of trial days */
1243 __('Free Trial: %d days', 'fluent-cart'),
1244 $trialDays
1245 );
1246 }
1247
1248 return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
1249 }
1250
1251
1252 public static function getCountryList(): array
1253 {
1254 $options = App::getInstance('localization')->countriesOptions();
1255 $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.');
1256
1257 return apply_filters('fluent_cart/util/countries', $options, []);
1258 }
1259
1260 public static function getCountyIsoLists(): array
1261 {
1262 return App::getInstance('localization')->getCountyIsoLists();
1263 }
1264
1265 public static function getCountryCode($country_name)
1266 {
1267 $countries = self::getCountryList();
1268 foreach ($countries as $country) {
1269 if ($country['name'] === $country_name) {
1270 return $country['value'];
1271 }
1272 }
1273 return '';
1274 }
1275
1276 /**
1277 * Get the country's name with country code,
1278 *
1279 * @param $code
1280 * @return string
1281 */
1282 public static function getCountryName($code): string
1283 {
1284 if (!$code || !is_string($code)) {
1285 return '';
1286 }
1287
1288 $countries = self::getCountyIsoLists();
1289
1290 return $countries[$code] ?? $code;
1291 }
1292
1293 public static function languageCodes(): array
1294 {
1295 return $langCodes = [
1296 'AF' => 'fa-AF',
1297 'AL' => 'sq-AL',
1298 'DZ' => 'ar-DZ',
1299 'AS' => 'sm-AS',
1300 'AD' => 'ca-AD',
1301 'AO' => 'pt-AO',
1302 'AR' => 'es-AR',
1303 'AM' => 'hy-AM',
1304 'AU' => 'en-AU',
1305 'AT' => 'de-AT',
1306 'AZ' => 'az-AZ',
1307 'BH' => 'ar-BH',
1308 'BD' => 'bn-BD',
1309 'BY' => 'be-BY',
1310 'BE' => 'nl-BE',
1311 'BZ' => 'en-BZ',
1312 'BJ' => 'fr-BJ',
1313 'BT' => 'dz-BT',
1314 'BO' => 'es-BO',
1315 'BA' => 'bs-BA',
1316 'BW' => 'en-BW',
1317 'BR' => 'pt-BR',
1318 'BN' => 'ms-BN',
1319 'BG' => 'bg-BG',
1320 'BF' => 'fr-BF',
1321 'BI' => 'fr-BI',
1322 'KH' => 'km-KH',
1323 'CM' => 'en-CM',
1324 'CA' => 'en-CA',
1325 'CV' => 'pt-CV',
1326 'CF' => 'fr-CF',
1327 'TD' => 'fr-TD',
1328 'CL' => 'es-CL',
1329 'CN' => 'zh-CN',
1330 'CO' => 'es-CO',
1331 'KM' => 'ar-KM',
1332 'CD' => 'fr-CD',
1333 'CG' => 'fr-CG',
1334 'CR' => 'es-CR',
1335 'CI' => 'fr-CI',
1336 'HR' => 'hr-HR',
1337 'CU' => 'es-CU',
1338 'CY' => 'el-CY',
1339 'CZ' => 'cs-CZ',
1340 'DK' => 'da-DK',
1341 'DJ' => 'fr-DJ',
1342 'DM' => 'en-DM',
1343 'DO' => 'es-DO',
1344 'EC' => 'es-EC',
1345 'EG' => 'ar-EG',
1346 'SV' => 'es-SV',
1347 'GQ' => 'es-GQ',
1348 'ER' => 'ti-ER',
1349 'EE' => 'et-EE',
1350 'ET' => 'am-ET',
1351 'FJ' => 'en-FJ',
1352 'FI' => 'fi-FI',
1353 'FR' => 'fr-FR',
1354 'GA' => 'fr-GA',
1355 'GM' => 'en-GM',
1356 'GE' => 'ka-GE',
1357 'DE' => 'de-DE',
1358 'GH' => 'en-GH',
1359 'GR' => 'el-GR',
1360 'GD' => 'en-GD',
1361 'GT' => 'es-GT',
1362 'GN' => 'fr-GN',
1363 'GW' => 'pt-GW',
1364 'GY' => 'en-GY',
1365 'HT' => 'fr-HT',
1366 'HN' => 'es-HN',
1367 'HU' => 'hu-HU',
1368 'IS' => 'is-IS',
1369 'IN' => 'hi-IN',
1370 'ID' => 'id-ID',
1371 'IR' => 'fa-IR',
1372 'IQ' => 'ar-IQ',
1373 'IE' => 'en-IE',
1374 'IL' => 'he-IL',
1375 'IT' => 'it-IT',
1376 'JM' => 'en-JM',
1377 'JP' => 'ja-JP',
1378 'JO' => 'ar-JO',
1379 'KZ' => 'kk-KZ',
1380 'KE' => 'sw-KE',
1381 'KI' => 'en-KI',
1382 'KR' => 'ko-KR',
1383 'KW' => 'ar-KW',
1384 'KG' => 'ky-KG',
1385 'LA' => 'lo-LA',
1386 'LV' => 'lv-LV',
1387 'LB' => 'ar-LB',
1388 'LS' => 'en-LS',
1389 'LR' => 'en-LR',
1390 'LY' => 'ar-LY',
1391 'LI' => 'de-LI',
1392 'LT' => 'lt-LT',
1393 'LU' => 'lb-LU',
1394 'MG' => 'mg-MG',
1395 'MW' => 'en-MW',
1396 'MY' => 'ms-MY',
1397 'MV' => 'dv-MV',
1398 'ML' => 'fr-ML',
1399 'MT' => 'mt-MT',
1400 'MH' => 'mh-MH',
1401 'MR' => 'ar-MR',
1402 'MU' => 'mfe-MU',
1403 'MX' => 'es-MX',
1404 'FM' => 'en-FM',
1405 'MD' => 'ro-MD',
1406 'MC' => 'fr-MC',
1407 'MN' => 'mn-MN',
1408 'ME' => 'sr-ME',
1409 'MA' => 'ar-MA',
1410 'MZ' => 'pt-MZ',
1411 'NA' => 'en-NA',
1412 'NR' => 'en-NR',
1413 'NP' => 'ne-NP',
1414 'NL' => 'nl-NL',
1415 'NZ' => 'en-NZ',
1416 'NI' => 'es-NI',
1417 'NG' => 'en-NG',
1418 'NO' => 'no-NO',
1419 'OM' => 'ar-OM',
1420 'PK' => 'ur-PK',
1421 'PW' => 'en-PW',
1422 'PA' => 'es-PA',
1423 'PG' => 'en-PG',
1424 'PY' => 'es-PY',
1425 'PE' => 'es-PE',
1426 'PH' => 'en-PH',
1427 'PL' => 'pl-PL',
1428 'PT' => 'pt-PT',
1429 'QA' => 'ar-QA',
1430 'RO' => 'ro-RO',
1431 'RU' => 'ru-RU',
1432 'RW' => 'rw-RW',
1433 'WS' => 'sm-WS',
1434 'SM' => 'it-SM',
1435 'SA' => 'ar-SA',
1436 'SN' => 'fr-SN',
1437 'RS' => 'sr-RS',
1438 'SC' => 'fr-SC',
1439 'SL' => 'en-SL',
1440 'SG' => 'en-SG',
1441 'SK' => 'sk-SK',
1442 'SI' => 'sl-SI',
1443 'SB' => 'en-SB',
1444 'SO' => 'so-SO',
1445 'ZA' => 'en-ZA',
1446 'ES' => 'es-ES',
1447 'LK' => 'si-LK',
1448 'SD' => 'ar-SD',
1449 'SR' => 'nl-SR',
1450 'SZ' => 'en-SZ',
1451 'SE' => 'sv-SE',
1452 'CH' => 'de-CH',
1453 'SY' => 'ar-SY',
1454 'TW' => 'zh-TW',
1455 'TJ' => 'tg-TJ',
1456 'TZ' => 'sw-TZ',
1457 'TH' => 'th-TH',
1458 'TL' => 'pt-TL',
1459 'TG' => 'fr-TG',
1460 'TO' => 'to-TO',
1461 'TT' => 'en-TT',
1462 'TN' => 'ar-TN',
1463 'TR' => 'tr-TR',
1464 'TM' => 'tk-TM',
1465 'TV' => 'en-TV',
1466 'UG' => 'en-UG',
1467 'UA' => 'uk-UA',
1468 'AE' => 'ar-AE',
1469 'GB' => 'en-GB',
1470 'US' => 'en-US',
1471 'UY' => 'es-UY',
1472 'UZ' => 'uz-UZ',
1473 'VU' => 'bi-VU',
1474 'VE' => 'es-VE',
1475 'VN' => 'vi-VN',
1476 'YE' => 'ar-YE',
1477 'ZM' => 'en-ZM',
1478 'ZW' => 'en-ZW'
1479 ];
1480 }
1481
1482 /**
1483 * Returns a translatable string with a shortcode inserted in the correct format.
1484 *
1485 * @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
1486 * @return string The formatted translatable string.
1487 */
1488 public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
1489 {
1490 $copyToClipboard = __('Copy to clipboard', 'fluent-cart');
1491 return sprintf(
1492 /* translators: %s: Shortcode */
1493 '<p>' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '</p>',
1494 '<code class="copyable-content" title="' . $copyToClipboard . '">' . ($shortcode) . '</code>',
1495 //$pageName
1496 );
1497 }
1498
1499
1500 /**
1501 * Get the current user Model.
1502 * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
1503 */
1504 public static function getCurrentUser()
1505 {
1506 static $user = false;
1507
1508 if ($user !== false) {
1509 return $user;
1510 }
1511
1512 $userId = get_current_user_id();
1513 if (!$userId) {
1514 $user = null;
1515 return $user;
1516 }
1517
1518 $user = User::query()->find($userId);
1519
1520 return $user;
1521
1522 }
1523
1524 public static function hasLicense($product): bool
1525 {
1526 if (empty($product)) {
1527 return false;
1528 }
1529
1530 $meta = Arr::get($product, 'licensesMeta.meta_value', []);
1531
1532 if (empty($meta)) {
1533 return false;
1534 }
1535
1536 $meta = is_string($meta) ? json_decode($meta, true) : $meta;
1537
1538 return Arr::get($meta, 'enabled') === 'yes';
1539
1540 }
1541
1542
1543 public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
1544 {
1545 $identifier = Arr::get($productDownload, 'download_identifier', '');
1546
1547 $validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
1548 'product_download' => $productDownload,
1549 'order_id' => $orderId,
1550 'is_admin' => $isAdmin,
1551 ]);
1552
1553 $signParams = [
1554 'download_identifier' => $identifier,
1555 'valid_till' => DateTime::now()
1556 ->addMinutes($validityInMinutes ?? 60)
1557 ->getTimestamp()
1558 ];
1559
1560 if ($orderId) {
1561 $orderId = Arr::wrap($orderId);
1562 $signParams['order_id'] = json_encode($orderId);
1563 }
1564
1565 $url = (new BaseUrl())->sign(site_url('/'), $signParams);
1566
1567 return URL::appendQueryParams($url, [
1568 'fluent-cart' => 'download-by-id',
1569 ]);
1570
1571 }
1572
1573
1574 public static function readableFileSize($bytes): string
1575 {
1576 // Converts bytes to a human-readable format (e.g., KB, MB, GB)
1577 // Example: 1024 -> "1 KB"
1578 // Example: 1048576 -> "1 MB"
1579
1580 if (!$bytes && $bytes !== 0) return '';
1581 $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1582 $i = floor(log($bytes, 2) / 10);
1583 $size = $bytes / pow(1024, $i);
1584 return round($size, 2) . ' ' . $units[$i];
1585 }
1586
1587
1588 public static function getSitePrefix()
1589 {
1590 $siteUrl = rtrim(home_url(), '/');
1591 // remove http:// or https:// from the URL
1592 $siteUrl = preg_replace('#^https?://#', '', $siteUrl);
1593 $sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
1594
1595 return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
1596 }
1597
1598 public static function humanIntervalMaps($interval = '')
1599 {
1600 $intervals = [
1601 'daily' => 'day',
1602 'weekly' => 'week',
1603 'monthly' => 'month',
1604 'yearly' => 'year'
1605 ];
1606
1607 return Arr::get($intervals, $interval);
1608 }
1609
1610 /**
1611 * @return array Array of intervals with label and value
1612 */
1613 public static function getAvailableSubscriptionIntervalOptions(): array
1614 {
1615 $intervals = [
1616 [
1617 'label' => __('Yearly', 'fluent-cart'),
1618 'value' => 'yearly',
1619 'map_value' => 'year',
1620 ],
1621 [
1622 'label' => __('Half Yearly', 'fluent-cart'),
1623 'value' => 'half_yearly',
1624 'map_value' => 'half_year',
1625 ],
1626 [
1627 'label' => __('Quarterly', 'fluent-cart'),
1628 'value' => 'quarterly',
1629 'map_value' => 'quarter',
1630 ],
1631 [
1632 'label' => __('Monthly', 'fluent-cart'),
1633 'value' => 'monthly',
1634 'map_value' => 'month',
1635 ],
1636 [
1637 'label' => __('Weekly', 'fluent-cart'),
1638 'value' => 'weekly',
1639 'map_value' => 'week',
1640 ],
1641 [
1642 'label' => __('Daily', 'fluent-cart'),
1643 'value' => 'daily',
1644 'map_value' => 'day',
1645 ]
1646 ];
1647
1648 return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
1649 }
1650
1651 public static function translateIntervalToStandardFormat($repeatInterval)
1652 {
1653 if (empty($repeatInterval)) {
1654 return 'year';
1655 }
1656
1657 $intervalMaps = static::getAvailableSubscriptionIntervalMaps();
1658
1659 if (!isset($intervalMaps[$repeatInterval])) {
1660 return 'year';
1661 }
1662
1663 return $intervalMaps[$repeatInterval];
1664 }
1665
1666 public static function getAvailableSubscriptionIntervalMaps()
1667 {
1668 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1669
1670 $intervalMaps = [];
1671 foreach ($intervalOptions as $option) {
1672 $intervalMaps[$option['value']] = $option['map_value'];
1673 }
1674
1675 return $intervalMaps;
1676
1677 }
1678
1679 public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
1680 {
1681 $intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
1682
1683 $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
1684 'existing_trial_days' => $trialDays,
1685 'repeat_interval' => $repeatInterval,
1686 'interval_in_days' => $intervalInDays,
1687 ]);
1688
1689 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
1690
1691 }
1692
1693 public static function subscriptionIntervalInDays($interval)
1694 {
1695 switch ($interval) {
1696 case 'daily':
1697 return 1;
1698 case 'weekly':
1699 return 7;
1700 case 'monthly':
1701 return 30;
1702 case 'quarterly':
1703 return 90;
1704 case 'half_yearly':
1705 return 182;
1706 case 'yearly':
1707 return 365;
1708 default:
1709 return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1710 'interval' => $interval,
1711 ]);
1712 }
1713 }
1714
1715 public static function parseTermIdsForFilter($filters): array
1716 {
1717 $taxonomies = Taxonomy::getTaxonomies();
1718 if (is_string($filters)) {
1719 $filters = json_decode($filters, true);
1720 }
1721 $formattedFilters = [];
1722
1723 foreach ($taxonomies as $key => $taxonomy) {
1724 $terms = Arr::get($filters, $key, []);
1725 if (!is_array($terms)) {
1726 $terms = [$terms];
1727 }
1728
1729 $terms = array_filter($terms, function ($term) {
1730 return !empty($term);
1731 });
1732
1733 if (!empty($terms)) {
1734 $terms = array_map(function ($term) {
1735 return sanitize_text_field((string)$term);
1736 }, $terms);
1737
1738
1739 $formattedFilters[$key] = $terms;
1740 }
1741
1742
1743 }
1744
1745 return $formattedFilters;
1746 }
1747
1748 public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
1749 {
1750 $result = [];
1751
1752 foreach ([$array1, $array2] as $array) {
1753 foreach ($array as $key => $values) {
1754 if (!isset($result[$key])) {
1755 $result[$key] = [];
1756 }
1757 $result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
1758 }
1759 }
1760
1761 return $result;
1762 }
1763
1764 public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
1765 {
1766 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1767
1768 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1769 $allChildVariants = Arr::except(
1770 $allChildVariants,
1771 Arr::pluck($variants, 'id')
1772 );
1773 $allChildVariants = array_filter($allChildVariants);
1774 $childVariants = ProductVariation::query()
1775 ->whereIn('id', $allChildVariants)
1776 ->select($select)
1777 ->get()
1778 ->toArray();
1779
1780
1781 foreach ($variants as &$variant) {
1782 $childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
1783 $variant['bundle_child_ids'] = $childIds;
1784 if (count($childIds) < 1) {
1785 $variant['child_variants'] = [];
1786 continue;
1787 }
1788 foreach ($childVariants as $childVariant) {
1789 if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
1790 $variant['child_variants'][$childVariant['id']] = $childVariant;
1791 }
1792 }
1793 }
1794
1795 return $variants;
1796 }
1797
1798 /**
1799 * Translate digits in a number according to the configured numeric system.
1800 *
1801 * Converts the digits 0-9 in the given number to their equivalents
1802 * defined in the numeric system string from `dateTimeStrings()`.
1803 * This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
1804 *
1805 * Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
1806 *
1807 * @param int|float|string $number The number to translate.
1808 * @return string The number with digits translated according to the configured numeric system.
1809 */
1810 public static function translateNumber($number): string
1811 {
1812 $config = TransStrings::dateTimeStrings();
1813 $numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
1814 $digits = explode('_', $numericSystem);
1815
1816 return strtr(
1817 (string)$number,
1818 array_combine(range(0,9),
1819 $digits)
1820 );
1821 }
1822
1823 public static function isModalCheckoutEnabled(): bool
1824 {
1825 //$storeSettings = new StoreSettings();
1826 //$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
1827 return apply_filters('fluent_cart/enable_modal_checkout', false);
1828 }
1829
1830 public static function isAdminUser(): bool
1831 {
1832 return current_user_can('manage_options');
1833 }
1834
1835 /**
1836 * Convert string/boolean to actual boolean value.
1837 * Handles shortcode string attributes like "true"/"false"
1838 *
1839 * @param mixed $value The value to convert to boolean
1840 * @param bool $default Default value if conversion fails
1841 * @return bool The boolean result
1842 */
1843 public static function toBool($value, bool $default = false): bool
1844 {
1845 if (is_bool($value)) {
1846 return $value;
1847 }
1848
1849 if (is_string($value)) {
1850 $value = strtolower(trim($value));
1851 if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
1852 return true;
1853 }
1854 if (in_array($value, ['false', '0', 'no', 'off'], true)) {
1855 return false;
1856 }
1857 }
1858
1859 return $default;
1860 }
1861 }
1862