PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
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.5.1, at app/Helpers/Helper.php

1,925 lines 63.3 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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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. It will be removed in v1.4.3.');
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 // advanced_variations is advertised in free too so the variation-type
712 // dropdown can offer it (shown Pro-locked with a crown and an upgrade
713 // modal while Pro is inactive). Pro re-registers the same key via the
714 // filter below when active.
715 $types = [
716 'simple' => __('Simple', 'fluent-cart'),
717 'simple_variations' => __('Simple Variations', 'fluent-cart'),
718 'advanced_variations' => __('Advanced Variations', 'fluent-cart'),
719 ];
720
721 $types = apply_filters('fluent_cart/variation_types', $types);
722
723 if ($withLabel) {
724 return $types;
725 }
726
727 return array_keys($types);
728 }
729
730 public static function isValueEncrypted($raw_value)
731 {
732 if (!$raw_value || !is_string($raw_value) || !extension_loaded('openssl')) {
733 return false;
734 }
735
736 // Check if input is valid base64
737 if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $raw_value)) {
738 return false;
739 }
740
741 $decoded = base64_decode($raw_value, true);
742 if ($decoded === false) {
743 return false;
744 }
745
746 // Check if decoded string is long enough for IV
747 $method = 'aes-256-ctr';
748 $ivlen = openssl_cipher_iv_length($method);
749 if (strlen($decoded) < $ivlen) {
750 return false;
751 }
752
753 $iv = substr($decoded, 0, $ivlen);
754 $ciphertext = substr($decoded, $ivlen);
755
756 $key = (defined('FLUENT_CART_ENCRYPTION_KEY'))
757 ? FLUENT_CART_ENCRYPTION_KEY
758 : ((defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY)
759 ? LOGGED_IN_KEY
760 : 'this-is-a-fallback-key-but-not-secure');
761 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT)
762 ? LOGGED_IN_SALT
763 : 'this-is-a-fallback-salt-but-not-secure';
764
765 $value = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
766 if ($value === false) {
767 return false;
768 }
769
770
771 return substr($value, -strlen($salt)) === $salt;
772 }
773
774 public static function encryptKey($value)
775 {
776 if (!$value) {
777 return $value;
778 }
779
780 if (!extension_loaded('openssl')) {
781 return $value;
782 }
783
784 if (self::isValueEncrypted($value)) {
785 return $value;
786 }
787
788 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
789
790 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
791 $key = FLUENT_CART_ENCRYPTION_KEY;
792 } else {
793 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
794 }
795
796 $method = 'aes-256-ctr';
797 $ivlen = openssl_cipher_iv_length($method);
798 $iv = openssl_random_pseudo_bytes($ivlen);
799
800 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
801 if (!$raw_value) {
802 return false;
803 }
804
805 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
806 }
807
808 public static function decryptKey($raw_value)
809 {
810
811 if (!$raw_value) {
812 return $raw_value;
813 }
814
815 if (!extension_loaded('openssl')) {
816 return $raw_value;
817 }
818
819 $decoded = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
820
821 if ($decoded === false) {
822 return $raw_value;
823 }
824
825 $method = 'aes-256-ctr';
826 $ivlen = openssl_cipher_iv_length($method);
827
828 if (strlen($decoded) <= $ivlen) {
829 return $raw_value;
830 }
831
832 $iv = substr($decoded, 0, $ivlen);
833
834 $ciphertext = substr($decoded, $ivlen);
835
836 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
837 $key = FLUENT_CART_ENCRYPTION_KEY;
838 } else {
839 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
840 }
841
842 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
843
844 $decrypted = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
845 if (!$decrypted || substr($decrypted, -strlen($salt)) !== $salt) {
846 return false;
847 }
848
849 return substr($decrypted, 0, -strlen($salt));
850 }
851
852 /**
853 * @return array
854 * For kses_post to allow some html tags
855 */
856 public static function allowedHTMLForCheckout(): array
857 {
858
859 $allowedTags = [
860 'svg' => [
861 'class' => true,
862 'aria-hidden' => true,
863 'aria-labelledby' => true,
864 'role' => true,
865 'xmlns' => true,
866 'width' => true,
867 'height' => true,
868 'viewbox' => true,
869 'fill' => true
870 ],
871 'g' => [
872 'fill' => true
873 ],
874 'title' => ['title' => true],
875 'path' => [
876 'd' => true,
877 'fill' => true,
878 'stroke' => true,
879 'fill-rule' => true,
880 'clip-rule' => true,
881 ],
882 ];
883
884 foreach (['input', 'label', 'div', 'span', 'p', 'select', 'option', 'textarea', 'button', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $tag) {
885 $allowedTags[$tag] = [
886 'selected',
887 'type' => [],
888 "selected='selected'" => [],
889 'name' => [],
890 'value' => [],
891 'autocomplete' => [],
892 'placeholder' => [],
893 'data-required' => [],
894 'data-type' => [],
895 'id' => [],
896 'class' => [],
897 'required' => [],
898 'disabled' => [],
899 'for' => [],
900 'style' => [],
901 'checked' => [],
902 'maxlength' => [],
903 'data-id' => [],
904 'data-country' => [],
905 'data-fluent-cart-checkout-page-form-address-modal-open-button' => [],
906 'data-fluent-cart-checkout-page-form-address-modal-wrapper' => [],
907 'data-fluent-cart-checkout-page-form-input-wrapper' => [],
908 'data-fluent_cart_checkout_error' => [],
909 'data-fluent-cart-checkout-page-form-address-modal-body' => [],
910 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button' => [],
911 'data-fluent-cart-checkout-page-form-address-input' => [],
912 'data-fluent-cart-checkout-page-form-address-select-wrapper' => [],
913 'data-fluent-cart-checkout-page-form-address-modal-close-button' => [],
914 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper' => [],
915 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-button' => [],
916 'data-fluent-cart-checkout-page-form-address-modal-apply-button' => [],
917 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-form-wrapper' => [],
918 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-submit-button' => [],
919 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-cancel-button' => [],
920 'data-fluent-cart-address-type' => [],
921 'data-fluent-cart-checkout-page-form-address-info-wrapper' => [],
922 'data-fluent-cart-checkout-page-form-error' => [],
923 'data-fluent-cart-checkout-page-form-section' => [],
924 'data-fluent-cart-checkout-page-discount-container' => [],
925 'data-fluent-cart-checkout-page-final-amount-container' => [],
926 'data-fluent-cart-checkout-page-new-total-amount' => [],
927 'data-fluent-cart-checkout-page-final-amount' => [],
928 'data-fluent-cart-checkout-page-coupon-validate' => [],
929 'data-fluent-cart-checkout-coupon-items-toggle' => [],
930 'data-fluent-cart-checkout-coupon-items-wrapper' => []
931
932
933 ];
934 }
935
936 return $allowedTags;
937 }
938
939 public static function getCouponStatuses()
940 {
941 $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [
942 [
943 'active' => __('Active', 'fluent-cart'),
944 'expired' => __('Expired', 'fluent-cart'),
945 'disabled' => __('Disabled', 'fluent-cart'),
946 ], []
947 ], '1.3.16', 'fluent_cart/coupon_statuses', 'Use fluent_cart/coupon_statuses instead of fluent-cart/coupon_statuses. It will be removed in v1.4.3.');
948
949 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
950 }
951
952 public static function getCouponSuccessStatuses()
953 {
954 return [
955 'active'
956 ];
957 }
958
959
960 /**
961 * Compact subscription terms as text
962 *
963 * Expected $data keys:
964 * - trial_days (int)
965 * - interval ('daily'|'weekly'|'monthly'|'yearly')
966 * - interval_count (int, default 1)
967 * - times (int; 0 = open-ended, >0 = finite number of payments/cycles)
968 * - price (string; formatted with currency)
969 * - signup_fee (string; 0 if none)
970 * - compare_price (string; 0 if none)
971 *
972 * Output examples:
973 * - "30 days free then $100.00 per year"
974 * - "$100 per year + $10 one-time signup fee"
975 * - "$100/month for 4 months"
976 * - "30 days free then $100/month for 4 months"
977 * - "$99 per month"
978 */
979 public static function getSubscriptionTermText(array $data, $asHtml = false): string
980 {
981 // Fixed interval options
982 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
983
984 // Normalize / defaults
985 $trialDays = $data['trial_days'] ?? 0;
986 $interval = (string)($data['interval'] ? $data['interval'] : 'monthly');
987
988 $unit = '';
989 if (isset($intervalOptions[$interval])) {
990 $unit = $intervalOptions[$interval];
991 } else if ($interval) {
992 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
993 foreach ($intervalOptions as $option) {
994 if ($option['value'] === $interval) {
995 $unit = strtolower($option['label']);
996 break;
997 }
998 }
999
1000 if (!$unit) {
1001 $unit = strtolower(str_replace(['_', '-'], ' ', $interval));
1002 }
1003 }
1004
1005 if (!$unit) {
1006 $unit = 'year';
1007 }
1008
1009 $count = max(1, (int)($data['interval_count'] ?? 1));
1010 $times = (int)($data['times'] ?? 0);
1011 $price = (string)($data['price'] ?? '');
1012 $signupFee = $data['signup_fee'] ?? '';
1013 $compare = $data['compare_price'] ?? '';
1014
1015 $signupFeeLabel = $data['signup_fee_label'] ?? __('signup fee', 'fluent-cart');
1016
1017 // helpers
1018 $pluralUnit = static function (string $unit, int $n): string {
1019 switch ($unit) {
1020 case 'day':
1021 return _n('day', 'days', $n, 'fluent-cart');
1022 case 'week':
1023 return _n('week', 'weeks', $n, 'fluent-cart');
1024 case 'quarter':
1025 return _n('quarter', 'quarters', $n, 'fluent-cart');
1026 case 'half_year':
1027 return _n('six month', 'six months', $n, 'fluent-cart');
1028 case 'year':
1029 return _n('year', 'years', $n, 'fluent-cart');
1030 case 'month':
1031 return _n('month', 'months', $n, 'fluent-cart');
1032 default:
1033 // For custom intervals, add 's' for plural
1034 return ($n > 1) ? $unit . 's' : $unit;
1035 }
1036 };
1037
1038 // Build the “per …” phrase (e.g., "per month", "per 3 months")
1039 $perPhrase = ($count === 1)
1040 ? sprintf(
1041 /* translators: %s is the singular unit name (e.g., day, month, year) */
1042 __('per %s', 'fluent-cart'),
1043 $pluralUnit($unit, 1)
1044 )
1045 : sprintf(
1046 /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
1047 __('per %1$d %2$s', 'fluent-cart'),
1048 $count,
1049 $pluralUnit($unit, $count)
1050 );
1051
1052 // Main price phrase:
1053 // - If monthly installments (times > 0 and unit == month and count == 1): "$X/month for N months"
1054 // - Else if times > 0: "$X per {count unit(s)} for N {cycle(s)}"
1055 // - Else: "$X per {count unit(s)}"
1056 if ($times > 0) {
1057 if ($unit === 'month' && $count === 1) {
1058 $installmentTail = sprintf(
1059 /* translators: %s: pluralized 'month(s)' */
1060 __('for %1$d %2$s', 'fluent-cart'),
1061 $times,
1062 $pluralUnit('month', $times)
1063 );
1064 $main = sprintf(
1065 /* translators: Compact monthly installment, e.g. "$100/month for 4 months" */
1066 __('%1$s/%2$s %3$s', 'fluent-cart'),
1067 $price,
1068 __('month', 'fluent-cart'),
1069 $installmentTail
1070 );
1071 } else {
1072 // Generic installment wording: "for N cycle(s)"
1073 $cyclesTail = sprintf(
1074 /* translators: %1$d is the number of cycles, %2$s is "cycle" or "cycles" */
1075 __('for %1$d %2$s', 'fluent-cart'),
1076 $times,
1077 _n('cycle', 'cycles', $times, 'fluent-cart')
1078 );
1079 $main = sprintf(
1080 /* translators: e.g. "$100 per 3 months for 4 cycles" */
1081 __('%1$s %2$s %3$s', 'fluent-cart'),
1082 $price,
1083 $perPhrase,
1084 $cyclesTail
1085 );
1086 }
1087 } else {
1088 // Open-ended subscription
1089 $main = sprintf(
1090 /* translators: e.g. "$99 per month" */
1091 __('%1$s %2$s', 'fluent-cart'),
1092 $price,
1093 $perPhrase
1094 );
1095 }
1096
1097 // Prefix trial if present: "N days free then …"
1098 if ($trialDays > 0) {
1099 $trialFrag = sprintf(
1100 /* translators: "30 days free then" */
1101 __('%1$d %2$s free then', 'fluent-cart'),
1102 $trialDays,
1103 $pluralUnit('day', $trialDays)
1104 );
1105 $main = $trialFrag . ' ' . $main;
1106 }
1107
1108 // Append signup fee if any: " + $10 one-time signup fee"
1109 if ($signupFee) {
1110 $main .= ' ' . sprintf(
1111 /* translators: e.g. "+ $10 one-time signup fee" */
1112 __('+ %1$s one-time %2$s', 'fluent-cart'),
1113 $signupFee,
1114 $signupFeeLabel
1115 );
1116 }
1117
1118 // No trailing period (matches your examples)
1119 return $main;
1120 }
1121
1122 public static function getTranslatedIntervalUnit(string $unit): string
1123 {
1124 switch ($unit) {
1125 case 'day':
1126 return __('day', 'fluent-cart');
1127 case 'week':
1128 return __('week', 'fluent-cart');
1129 case 'month':
1130 return __('month', 'fluent-cart');
1131 case 'quarter':
1132 return __('quarter', 'fluent-cart');
1133 case 'half_year':
1134 return __('six month', 'fluent-cart');
1135 case 'year':
1136 return __('year', 'fluent-cart');
1137 default:
1138 return $unit;
1139 }
1140 }
1141
1142 public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string
1143 {
1144 // Convert to array only if it's an object
1145 if (is_object($otherInfo)) {
1146 $otherInfo = json_decode(json_encode($otherInfo), true);
1147 }
1148
1149 $price = self::toDecimal($itemPrice, true, $currencyCode);
1150 $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
1151
1152 if ($recurringDiscountAmount) {
1153 $newRecurringAmount = $itemPrice - $recurringDiscountAmount;
1154 $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode);
1155 }
1156
1157 $repeatInterval = Arr::get($otherInfo, 'repeat_interval', '');
1158 $occurrence = (int)Arr::get($otherInfo, 'times', 0);
1159
1160 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1161
1162 $intervalUnit = '';
1163 if (isset($intervalOptions[$repeatInterval])) {
1164 $intervalUnit = $intervalOptions[$repeatInterval];
1165 } else if ($repeatInterval) {
1166 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1167 foreach ($intervalOptions as $option) {
1168 if ($option['value'] === $repeatInterval) {
1169 $intervalUnit = strtolower($option['label']);
1170 break;
1171 }
1172 }
1173
1174 if (!$intervalUnit) {
1175 $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval));
1176 }
1177 }
1178
1179 $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit);
1180
1181 $interval = $intervalUnit
1182 ? sprintf(
1183 /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */
1184 __('per %s', 'fluent-cart'),
1185 $intervalLabel
1186 )
1187 : '';
1188
1189 $time = $intervalUnit
1190 ? $intervalLabel
1191 : '';
1192
1193 $paymentInfo = sprintf(
1194 /* 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 */
1195 __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
1196 $price,
1197 $interval,
1198 $occurrence,
1199 $time
1200 );
1201
1202 if (empty($occurrence)) {
1203 $paymentInfo = sprintf(
1204 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
1205 __('%1$s %2$s %3$s', 'fluent-cart'),
1206 $price,
1207 $interval,
1208 __('until cancel', 'fluent-cart')
1209 );
1210 }
1211
1212 return !empty($otherInfo) ? $paymentInfo : null;
1213 }
1214
1215 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1216 {
1217 // Convert to array if it's an object
1218 if (is_object($otherInfo)) {
1219 $otherInfo = json_decode(json_encode($otherInfo), true);
1220 }
1221
1222 $signupFeeName = Arr::get($otherInfo, 'signup_fee_name', __('Setup Fee', 'fluent-cart'));
1223 $fee = Arr::get($otherInfo, 'signup_fee', 0);
1224 $manageSetupFee = Arr::get($otherInfo, 'manage_setup_fee', 'no');
1225
1226 if ($manageSetupFee !== 'yes' || !$fee) {
1227 return '';
1228 }
1229
1230
1231 if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) {
1232 if ($fee != $originalSetupFee) {
1233 $title = __('Adjusted setup fee', 'fluent-cart');
1234 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1235
1236 if ($asArray) {
1237 return [
1238 'signup_fee_name' => $title,
1239 'signup_fee' => $fee,
1240 'signup_fee_formatted' => $formattedAmount,
1241 ];
1242 }
1243 return $title . $formattedAmount;
1244 }
1245 }
1246
1247 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1248 if ($asArray) {
1249 return [
1250 'signup_fee_name' => $signupFeeName,
1251 'signup_fee' => $fee,
1252 'signup_fee_formatted' => $formattedAmount,
1253 ];
1254 }
1255
1256
1257 return $signupFeeName . ' ' . $formattedAmount;
1258 }
1259
1260 public static function generateTrialInfo($otherInfo)
1261 {
1262 $trialInfo = '';
1263
1264 $trialDays = Arr::get($otherInfo, 'trial_days', 0);
1265
1266 if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
1267 $trialInfo = sprintf(
1268 /* translators: %d is the number of trial days */
1269 __('Free Trial: %d days', 'fluent-cart'),
1270 $trialDays
1271 );
1272 }
1273
1274 return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
1275 }
1276
1277
1278 public static function getCountryList(): array
1279 {
1280 $options = App::getInstance('localization')->countriesOptions();
1281 $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. It will be removed in v1.4.3.');
1282
1283 return apply_filters('fluent_cart/util/countries', $options, []);
1284 }
1285
1286 public static function getCountyIsoLists(): array
1287 {
1288 return App::getInstance('localization')->getCountyIsoLists();
1289 }
1290
1291 public static function getCountryCode($country_name)
1292 {
1293 $countries = self::getCountryList();
1294 foreach ($countries as $country) {
1295 if ($country['name'] === $country_name) {
1296 return $country['value'];
1297 }
1298 }
1299 return '';
1300 }
1301
1302 /**
1303 * Get the country's name with country code,
1304 *
1305 * @param $code
1306 * @return string
1307 */
1308 public static function getCountryName($code): string
1309 {
1310 if (!$code || !is_string($code)) {
1311 return '';
1312 }
1313
1314 $countries = self::getCountyIsoLists();
1315
1316 return $countries[$code] ?? $code;
1317 }
1318
1319 public static function languageCodes(): array
1320 {
1321 return $langCodes = [
1322 'AF' => 'fa-AF',
1323 'AL' => 'sq-AL',
1324 'DZ' => 'ar-DZ',
1325 'AS' => 'sm-AS',
1326 'AD' => 'ca-AD',
1327 'AO' => 'pt-AO',
1328 'AR' => 'es-AR',
1329 'AM' => 'hy-AM',
1330 'AU' => 'en-AU',
1331 'AT' => 'de-AT',
1332 'AZ' => 'az-AZ',
1333 'BH' => 'ar-BH',
1334 'BD' => 'bn-BD',
1335 'BY' => 'be-BY',
1336 'BE' => 'nl-BE',
1337 'BZ' => 'en-BZ',
1338 'BJ' => 'fr-BJ',
1339 'BT' => 'dz-BT',
1340 'BO' => 'es-BO',
1341 'BA' => 'bs-BA',
1342 'BW' => 'en-BW',
1343 'BR' => 'pt-BR',
1344 'BN' => 'ms-BN',
1345 'BG' => 'bg-BG',
1346 'BF' => 'fr-BF',
1347 'BI' => 'fr-BI',
1348 'KH' => 'km-KH',
1349 'CM' => 'en-CM',
1350 'CA' => 'en-CA',
1351 'CV' => 'pt-CV',
1352 'CF' => 'fr-CF',
1353 'TD' => 'fr-TD',
1354 'CL' => 'es-CL',
1355 'CN' => 'zh-CN',
1356 'CO' => 'es-CO',
1357 'KM' => 'ar-KM',
1358 'CD' => 'fr-CD',
1359 'CG' => 'fr-CG',
1360 'CR' => 'es-CR',
1361 'CI' => 'fr-CI',
1362 'HR' => 'hr-HR',
1363 'CU' => 'es-CU',
1364 'CY' => 'el-CY',
1365 'CZ' => 'cs-CZ',
1366 'DK' => 'da-DK',
1367 'DJ' => 'fr-DJ',
1368 'DM' => 'en-DM',
1369 'DO' => 'es-DO',
1370 'EC' => 'es-EC',
1371 'EG' => 'ar-EG',
1372 'SV' => 'es-SV',
1373 'GQ' => 'es-GQ',
1374 'ER' => 'ti-ER',
1375 'EE' => 'et-EE',
1376 'ET' => 'am-ET',
1377 'FJ' => 'en-FJ',
1378 'FI' => 'fi-FI',
1379 'FR' => 'fr-FR',
1380 'GA' => 'fr-GA',
1381 'GM' => 'en-GM',
1382 'GE' => 'ka-GE',
1383 'DE' => 'de-DE',
1384 'GH' => 'en-GH',
1385 'GR' => 'el-GR',
1386 'GD' => 'en-GD',
1387 'GT' => 'es-GT',
1388 'GN' => 'fr-GN',
1389 'GW' => 'pt-GW',
1390 'GY' => 'en-GY',
1391 'HT' => 'fr-HT',
1392 'HN' => 'es-HN',
1393 'HU' => 'hu-HU',
1394 'IS' => 'is-IS',
1395 'IN' => 'hi-IN',
1396 'ID' => 'id-ID',
1397 'IR' => 'fa-IR',
1398 'IQ' => 'ar-IQ',
1399 'IE' => 'en-IE',
1400 'IL' => 'he-IL',
1401 'IT' => 'it-IT',
1402 'JM' => 'en-JM',
1403 'JP' => 'ja-JP',
1404 'JO' => 'ar-JO',
1405 'KZ' => 'kk-KZ',
1406 'KE' => 'sw-KE',
1407 'KI' => 'en-KI',
1408 'KR' => 'ko-KR',
1409 'KW' => 'ar-KW',
1410 'KG' => 'ky-KG',
1411 'LA' => 'lo-LA',
1412 'LV' => 'lv-LV',
1413 'LB' => 'ar-LB',
1414 'LS' => 'en-LS',
1415 'LR' => 'en-LR',
1416 'LY' => 'ar-LY',
1417 'LI' => 'de-LI',
1418 'LT' => 'lt-LT',
1419 'LU' => 'lb-LU',
1420 'MG' => 'mg-MG',
1421 'MW' => 'en-MW',
1422 'MY' => 'ms-MY',
1423 'MV' => 'dv-MV',
1424 'ML' => 'fr-ML',
1425 'MT' => 'mt-MT',
1426 'MH' => 'mh-MH',
1427 'MR' => 'ar-MR',
1428 'MU' => 'mfe-MU',
1429 'MX' => 'es-MX',
1430 'FM' => 'en-FM',
1431 'MD' => 'ro-MD',
1432 'MC' => 'fr-MC',
1433 'MN' => 'mn-MN',
1434 'ME' => 'sr-ME',
1435 'MA' => 'ar-MA',
1436 'MZ' => 'pt-MZ',
1437 'NA' => 'en-NA',
1438 'NR' => 'en-NR',
1439 'NP' => 'ne-NP',
1440 'NL' => 'nl-NL',
1441 'NZ' => 'en-NZ',
1442 'NI' => 'es-NI',
1443 'NG' => 'en-NG',
1444 'NO' => 'no-NO',
1445 'OM' => 'ar-OM',
1446 'PK' => 'ur-PK',
1447 'PW' => 'en-PW',
1448 'PA' => 'es-PA',
1449 'PG' => 'en-PG',
1450 'PY' => 'es-PY',
1451 'PE' => 'es-PE',
1452 'PH' => 'en-PH',
1453 'PL' => 'pl-PL',
1454 'PT' => 'pt-PT',
1455 'QA' => 'ar-QA',
1456 'RO' => 'ro-RO',
1457 'RU' => 'ru-RU',
1458 'RW' => 'rw-RW',
1459 'WS' => 'sm-WS',
1460 'SM' => 'it-SM',
1461 'SA' => 'ar-SA',
1462 'SN' => 'fr-SN',
1463 'RS' => 'sr-RS',
1464 'SC' => 'fr-SC',
1465 'SL' => 'en-SL',
1466 'SG' => 'en-SG',
1467 'SK' => 'sk-SK',
1468 'SI' => 'sl-SI',
1469 'SB' => 'en-SB',
1470 'SO' => 'so-SO',
1471 'ZA' => 'en-ZA',
1472 'ES' => 'es-ES',
1473 'LK' => 'si-LK',
1474 'SD' => 'ar-SD',
1475 'SR' => 'nl-SR',
1476 'SZ' => 'en-SZ',
1477 'SE' => 'sv-SE',
1478 'CH' => 'de-CH',
1479 'SY' => 'ar-SY',
1480 'TW' => 'zh-TW',
1481 'TJ' => 'tg-TJ',
1482 'TZ' => 'sw-TZ',
1483 'TH' => 'th-TH',
1484 'TL' => 'pt-TL',
1485 'TG' => 'fr-TG',
1486 'TO' => 'to-TO',
1487 'TT' => 'en-TT',
1488 'TN' => 'ar-TN',
1489 'TR' => 'tr-TR',
1490 'TM' => 'tk-TM',
1491 'TV' => 'en-TV',
1492 'UG' => 'en-UG',
1493 'UA' => 'uk-UA',
1494 'AE' => 'ar-AE',
1495 'GB' => 'en-GB',
1496 'US' => 'en-US',
1497 'UY' => 'es-UY',
1498 'UZ' => 'uz-UZ',
1499 'VU' => 'bi-VU',
1500 'VE' => 'es-VE',
1501 'VN' => 'vi-VN',
1502 'YE' => 'ar-YE',
1503 'ZM' => 'en-ZM',
1504 'ZW' => 'en-ZW'
1505 ];
1506 }
1507
1508 /**
1509 * Returns a translatable string with a shortcode inserted in the correct format.
1510 *
1511 * @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
1512 * @return string The formatted translatable string.
1513 */
1514 public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
1515 {
1516 $copyToClipboard = __('Copy to clipboard', 'fluent-cart');
1517 return sprintf(
1518 /* translators: %s: Shortcode */
1519 '<p>' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '</p>',
1520 '<code class="copyable-content" title="' . $copyToClipboard . '">' . ($shortcode) . '</code>',
1521 //$pageName
1522 );
1523 }
1524
1525
1526 /**
1527 * Get the current user Model.
1528 * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
1529 */
1530 public static function getCurrentUser($refresh = false)
1531 {
1532 static $user = false;
1533
1534 if (!$refresh && $user !== false) {
1535 return $user;
1536 }
1537
1538 $userId = get_current_user_id();
1539 if (!$userId) {
1540 $user = null;
1541 return $user;
1542 }
1543
1544 $user = User::query()->find($userId);
1545
1546 return $user;
1547
1548 }
1549
1550 public static function hasLicense($product): bool
1551 {
1552 if (empty($product)) {
1553 return false;
1554 }
1555
1556 $meta = Arr::get($product, 'licensesMeta.meta_value', []);
1557
1558 if (empty($meta)) {
1559 return false;
1560 }
1561
1562 $meta = is_string($meta) ? json_decode($meta, true) : $meta;
1563
1564 return Arr::get($meta, 'enabled') === 'yes';
1565
1566 }
1567
1568
1569 public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
1570 {
1571 $identifier = Arr::get($productDownload, 'download_identifier', '');
1572
1573 $validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
1574 'product_download' => $productDownload,
1575 'order_id' => $orderId,
1576 'is_admin' => $isAdmin,
1577 ]);
1578
1579 $signParams = [
1580 'download_identifier' => $identifier,
1581 'valid_till' => DateTime::now()
1582 ->addMinutes($validityInMinutes ?? 60)
1583 ->getTimestamp()
1584 ];
1585
1586 if ($orderId) {
1587 $orderId = Arr::wrap($orderId);
1588 $signParams['order_id'] = json_encode($orderId);
1589 }
1590
1591 $url = (new BaseUrl())->sign(site_url('/'), $signParams);
1592
1593 return URL::appendQueryParams($url, [
1594 'fluent-cart' => 'download-by-id',
1595 ]);
1596
1597 }
1598
1599
1600 public static function readableFileSize($bytes): string
1601 {
1602 // Converts bytes to a human-readable format (e.g., KB, MB, GB)
1603 // Example: 1024 -> "1 KB"
1604 // Example: 1048576 -> "1 MB"
1605
1606 if (!$bytes && $bytes !== 0) return '';
1607 $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1608 $i = floor(log($bytes, 2) / 10);
1609 $size = $bytes / pow(1024, $i);
1610 return round($size, 2) . ' ' . $units[$i];
1611 }
1612
1613
1614 public static function getSitePrefix()
1615 {
1616 $siteUrl = rtrim(home_url(), '/');
1617 // remove http:// or https:// from the URL
1618 $siteUrl = preg_replace('#^https?://#', '', $siteUrl);
1619 $sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
1620
1621 return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
1622 }
1623
1624 public static function humanIntervalMaps($interval = '')
1625 {
1626 $intervals = [
1627 'daily' => __('day', 'fluent-cart'),
1628 'weekly' => __('week', 'fluent-cart'),
1629 'monthly' => __('month', 'fluent-cart'),
1630 'quarterly' => __('quarter', 'fluent-cart'),
1631 'half_yearly' => __('six month', 'fluent-cart'),
1632 'yearly' => __('year', 'fluent-cart'),
1633 ];
1634
1635 return Arr::get($intervals, $interval);
1636 }
1637
1638 /**
1639 * @return array Array of intervals with label and value
1640 */
1641 public static function getAvailableSubscriptionIntervalOptions(): array
1642 {
1643 $intervals = [
1644 [
1645 'label' => __('Yearly', 'fluent-cart'),
1646 'value' => 'yearly',
1647 'map_value' => 'year',
1648 ],
1649 [
1650 'label' => __('Half Yearly', 'fluent-cart'),
1651 'value' => 'half_yearly',
1652 'map_value' => 'half_year',
1653 ],
1654 [
1655 'label' => __('Quarterly', 'fluent-cart'),
1656 'value' => 'quarterly',
1657 'map_value' => 'quarter',
1658 ],
1659 [
1660 'label' => __('Monthly', 'fluent-cart'),
1661 'value' => 'monthly',
1662 'map_value' => 'month',
1663 ],
1664 [
1665 'label' => __('Weekly', 'fluent-cart'),
1666 'value' => 'weekly',
1667 'map_value' => 'week',
1668 ],
1669 [
1670 'label' => __('Daily', 'fluent-cart'),
1671 'value' => 'daily',
1672 'map_value' => 'day',
1673 ]
1674 ];
1675
1676 return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
1677 }
1678
1679 public static function translateIntervalToStandardFormat($repeatInterval)
1680 {
1681 if (empty($repeatInterval)) {
1682 return 'year';
1683 }
1684
1685 $intervalMaps = static::getAvailableSubscriptionIntervalMaps();
1686
1687 if (!isset($intervalMaps[$repeatInterval])) {
1688 return 'year';
1689 }
1690
1691 return $intervalMaps[$repeatInterval];
1692 }
1693
1694 public static function getAvailableSubscriptionIntervalMaps()
1695 {
1696 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1697
1698 $intervalMaps = [];
1699 foreach ($intervalOptions as $option) {
1700 $intervalMaps[$option['value']] = $option['map_value'];
1701 }
1702
1703 return $intervalMaps;
1704
1705 }
1706
1707 public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
1708 {
1709 $intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
1710
1711 $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
1712 'existing_trial_days' => $trialDays,
1713 'repeat_interval' => $repeatInterval,
1714 'interval_in_days' => $intervalInDays,
1715 ]);
1716
1717 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
1718
1719 }
1720
1721 public static function subscriptionIntervalInDays($interval)
1722 {
1723 switch ($interval) {
1724 case 'daily':
1725 return 1;
1726 case 'weekly':
1727 return 7;
1728 case 'monthly':
1729 return 30;
1730 case 'quarterly':
1731 return 90;
1732 case 'half_yearly':
1733 return 182;
1734 case 'yearly':
1735 return 365;
1736 default:
1737 return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1738 'interval' => $interval,
1739 ]);
1740 }
1741 }
1742
1743 public static function parseTermIdsForFilter($filters): array
1744 {
1745 $taxonomies = Taxonomy::getTaxonomies();
1746 if (is_string($filters)) {
1747 $filters = json_decode($filters, true);
1748 }
1749 $formattedFilters = [];
1750
1751 foreach ($taxonomies as $key => $taxonomy) {
1752 $terms = Arr::get($filters, $key, []);
1753 if (!is_array($terms)) {
1754 $terms = [$terms];
1755 }
1756
1757 $terms = array_filter($terms, function ($term) {
1758 return !empty($term);
1759 });
1760
1761 if (!empty($terms)) {
1762 $terms = array_map(function ($term) {
1763 return sanitize_text_field((string)$term);
1764 }, $terms);
1765
1766
1767 $formattedFilters[$key] = $terms;
1768 }
1769
1770
1771 }
1772
1773 return $formattedFilters;
1774 }
1775
1776 public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
1777 {
1778 $result = [];
1779
1780 foreach ([$array1, $array2] as $array) {
1781 foreach ($array as $key => $values) {
1782 if (!isset($result[$key])) {
1783 $result[$key] = [];
1784 }
1785 $result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
1786 }
1787 }
1788
1789 return $result;
1790 }
1791
1792 public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
1793 {
1794 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1795
1796 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1797 $allChildVariants = Arr::except(
1798 $allChildVariants,
1799 Arr::pluck($variants, 'id')
1800 );
1801 $allChildVariants = array_filter($allChildVariants);
1802 $childVariants = ProductVariation::query()
1803 ->whereIn('id', $allChildVariants)
1804 ->with('product:ID,post_title')
1805 ->select($select)
1806 ->get()
1807 ->toArray();
1808
1809 // Extract post_title from product and remove product object to keep data clean
1810 foreach ($childVariants as $key => $childVariant) {
1811 $postTitle = Arr::get($childVariant, 'product.post_title');
1812 if ($postTitle) {
1813 $childVariants[$key]['post_title'] = $postTitle;
1814 unset($childVariants[$key]['product']);
1815 }
1816 }
1817
1818 foreach ($variants as &$variant) {
1819 $childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
1820 $variant['bundle_child_ids'] = $childIds;
1821 if (count($childIds) < 1) {
1822 $variant['child_variants'] = [];
1823 continue;
1824 }
1825 foreach ($childVariants as $childVariant) {
1826 if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
1827 $variant['child_variants'][$childVariant['id']] = $childVariant;
1828 }
1829 }
1830 }
1831
1832 return $variants;
1833 }
1834
1835 /**
1836 * Translate digits in a number according to the configured numeric system.
1837 *
1838 * Converts the digits 0-9 in the given number to their equivalents
1839 * defined in the numeric system string from `dateTimeStrings()`.
1840 * This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
1841 *
1842 * Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
1843 *
1844 * @param int|float|string $number The number to translate.
1845 * @return string The number with digits translated according to the configured numeric system.
1846 */
1847 public static function translateNumber($number): string
1848 {
1849 $config = TransStrings::dateTimeStrings();
1850 $numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
1851 $digits = explode('_', $numericSystem);
1852
1853 return strtr(
1854 (string)$number,
1855 array_combine(range(0, 9),
1856 $digits)
1857 );
1858 }
1859
1860 public static function isModalCheckoutEnabled(): bool
1861 {
1862 //$storeSettings = new StoreSettings();
1863 //$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
1864 return apply_filters('fluent_cart/enable_modal_checkout', false);
1865 }
1866
1867 public static function isAdminUser(): bool
1868 {
1869 return current_user_can('manage_options');
1870 }
1871
1872 /**
1873 * Convert string/boolean to actual boolean value.
1874 * Handles shortcode string attributes like "true"/"false"
1875 *
1876 * @param mixed $value The value to convert to boolean
1877 * @param bool $default Default value if conversion fails
1878 * @return bool The boolean result
1879 */
1880 public static function toBool($value, bool $default = false): bool
1881 {
1882 if (is_bool($value)) {
1883 return $value;
1884 }
1885
1886 if (is_string($value)) {
1887 $value = strtolower(trim($value));
1888 if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
1889 return true;
1890 }
1891 if (in_array($value, ['false', '0', 'no', 'off'], true)) {
1892 return false;
1893 }
1894 }
1895
1896 return $default;
1897 }
1898
1899 public static function formatTaxRatePercent(float $rate): string
1900 {
1901 $formatted = number_format($rate, 4, '.', '');
1902 if (strpos($formatted, '.') !== false) {
1903 $formatted = rtrim($formatted, '0');
1904 $formatted = rtrim($formatted, '.');
1905 }
1906 return $formatted;
1907 }
1908
1909 /**
1910 * Returns the tax label for order-level tax rows (tax_total, not per-item).
1911 * For mixed orders, indicates tax varies per item.
1912 *
1913 * @param \FluentCart\App\Models\Order $order
1914 * @return string
1915 */
1916 public static function getOrderTaxLabel($order) {
1917 if ((int) $order->tax_behavior === 3) {
1918 return esc_html__('(Varies)', 'fluent-cart');
1919 }
1920 return (int) $order->tax_behavior === 2
1921 ? esc_html__('(Included)', 'fluent-cart')
1922 : esc_html__('(Excluded)', 'fluent-cart');
1923 }
1924 }
1925