PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 / api / StoreSettings.php

StoreSettings.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at api/StoreSettings.php

2,198 lines 111.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Api;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Vite;
7 use FluentCart\App\CPT\Pages;
8 use FluentCart\App\Helpers\AddressHelper;
9 use FluentCart\App\Helpers\CurrenciesHelper;
10 use FluentCart\App\Services\OrderService;
11 use FluentCart\App\Services\Theme\ColorPalette;
12 use FluentCart\App\Services\Theme\ThemePalette;
13 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
14 use FluentCart\App\Modules\StoreManagedRenewal\Services\RenewalService;
15 use FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode;
16 use FluentCart\Framework\Support\Arr;
17 use FluentCart\Framework\Support\ArrayableInterface;
18 use FluentCart\Framework\Support\Str;
19 use FluentCart\App\Services\Permission\PermissionManager;
20
21 class StoreSettings implements ArrayableInterface
22 {
23 const CACHE_KEY = 'store_settings';
24 const CACHE_GROUP = 'fluentcart';
25
26 /**
27 * @var string
28 *
29 * Store settings option name
30 */
31 protected string $optionKey = 'fluent_cart_store_settings';
32
33 /**
34 * @var array key value pair
35 *
36 * Store settings parsed from fields
37 */
38 protected array $storeSettings;
39
40 protected static $cachedStoreSettings = null;
41
42 public static function clearCache(): void
43 {
44 self::$cachedStoreSettings = null;
45 wp_cache_delete(self::CACHE_KEY, self::CACHE_GROUP);
46 }
47
48 public function __construct()
49 {
50 if (self::$cachedStoreSettings !== null) {
51 $this->storeSettings = self::$cachedStoreSettings;
52 return;
53 }
54 $defaultSettings = $this->getDefaultSettings();
55 $storeSettings = get_option($this->optionKey, []);
56 $settings = wp_parse_args($storeSettings, $defaultSettings);
57 $this->storeSettings = $settings;
58 self::$cachedStoreSettings = $this->storeSettings;
59 }
60
61 protected function getDefaultSettings(): array
62 {
63 $defaultSettings = [
64 'store_name' => get_bloginfo('name'),
65 'company_name' => '',
66 'legal_registration_id' => '',
67 'seller_vat_id' => '',
68 'seller_tax_id' => '',
69 'note_for_user_account_creation' => __('An user account will be created', 'fluent-cart'),
70 'checkout_button_text' => __('Checkout', 'fluent-cart'),
71 'view_cart_button_text' => __('View Cart', 'fluent-cart'),
72 'cart_button_text' => __('Add To Cart', 'fluent-cart'),
73 'popup_button_text' => __('View Product', 'fluent-cart'),
74 'out_of_stock_button_text' => __('Not Available', 'fluent-cart'),
75 'currency_position' => 'before',
76 // 'thousand_separator' => 'comma',
77 'decimal_separator' => 'dot',
78 'checkout_method_style' => 'logo',
79 'enable_modal_checkout' => 'no',
80 'require_logged_in' => 'no',
81 'show_cart_icon_in_nav' => 'no',
82 'show_cart_icon_in_body' => 'yes',
83 'additional_address_field' => 'yes',
84 'hide_coupon_field' => 'no',
85 'user_account_creation_mode' => 'all',
86 'auto_login_after_account_creation' => 'no',
87 'checkout_page_id' => '',
88 'custom_payment_page_id' => '',
89 'registration_page_id' => '',
90 'login_page_id' => '',
91 'cart_page_id' => '',
92 'receipt_page_id' => '',
93 'shop_page_id' => '',
94 'customer_profile_page_id' => '',
95 'customer_profile_page_slug' => '',
96 'currency' => 'USD',
97 'store_address1' => '',
98 'store_address2' => '',
99 'store_city' => '',
100 'store_country' => '',
101 'store_postcode' => '',
102 'store_state' => '',
103 'show_relevant_product_in_single_page' => 'yes',
104 'show_relevant_product_in_modal' => '',
105 'order_mode' => 'test',
106 'subscription_mode_guard' => 'yes',
107 'variation_view' => 'both',
108 'variation_columns' => 'masonry',
109 'enable_early_payment_for_installment' => 'yes',
110 'subscription_management_mode' => 'gateway_managed',
111 'subscription_system_charge' => 'no',
112 'modules_settings' => [],
113 'min_receipt_number' => '1',
114 'inv_prefix' => 'INV-',
115 'weight_unit' => 'kg',
116 'dimension_unit' => 'cm',
117 'appearance_source' => ColorPalette::SOURCE_DEFAULT,
118 'appearance_colors' => [],
119 // 'wordpress', not 'fluent_cart': the FluentCart patterns are
120 // literals ('M j, Y'), and a literal renders a half-translated date
121 // on a localized store -- a German month in English field order,
122 // which a German reader misreads as day-first. Following
123 // Settings > General is the only source that is correct in every
124 // locale, so it is what a store gets until it chooses otherwise.
125 'date_time_format_source' => 'wordpress',
126 'timezone_source' => 'fluent_cart'
127 ];
128
129 return apply_filters('fluent_cart/store_settings/values', $defaultSettings, []);
130 }
131
132 /**
133 * @return array
134 *
135 * Get all store settings fields
136 * @hook to use apply_filters("fluent_cart/store_setting_fields", $fields)
137 */
138 public function fields($params = []): array
139 {
140
141 $pages = Pages::getPages('');
142 $previewLinks = [
143 'shop_page_id' => $this->getShopPage(),
144 'customer_profile_page_id' => $this->getCustomerProfilePage(),
145 'cart_page_id' => $this->getCartPage(),
146 'checkout_page_id' => $this->getCheckoutPage(),
147 'receipt_page_id' => $this->getReceiptPage()
148 ];
149 $isProActive = App::isProActive();
150 $proFeatureIcon = Vite::getAssetUrl('images/crown.svg');
151
152 // Read-only schedule for store-managed subscription renewals. Pulled live
153 // from the same map the scheduler uses, so it stays accurate under the
154 // fluent_cart/renewal/advance_creation_days filter. No editable knob — the timing is
155 // deliberately built-in; developers tune it via that filter.
156 $invoiceScheduleMap = RenewalService::getAdvanceCreationDaysMap();
157 $invoiceScheduleLabels = [
158 'daily' => __('Daily', 'fluent-cart'),
159 'weekly' => __('Weekly', 'fluent-cart'),
160 'monthly' => __('Monthly', 'fluent-cart'),
161 'quarterly' => __('Quarterly', 'fluent-cart'),
162 'half_yearly' => __('Half-yearly', 'fluent-cart'),
163 'yearly' => __('Yearly', 'fluent-cart'),
164 ];
165 // Structured renewal-order schedule for the SubscriptionModeManager
166 // component (status card + guarded edit dialog).
167 $invoiceScheduleList = [];
168 foreach ($invoiceScheduleLabels as $invoiceScheduleKey => $invoiceScheduleLabel) {
169 if (!isset($invoiceScheduleMap[$invoiceScheduleKey])) {
170 continue;
171 }
172 $invoiceScheduleDays = (int) $invoiceScheduleMap[$invoiceScheduleKey];
173 $invoiceScheduleList[] = [
174 'label' => $invoiceScheduleLabel,
175 'when' => $invoiceScheduleDays <= 0
176 ? __('on the due date', 'fluent-cart')
177 /* translators: %d: number of days before the due date */
178 : sprintf(_n('%d day before due date', '%d days before due date', $invoiceScheduleDays, 'fluent-cart'), $invoiceScheduleDays),
179 ];
180 }
181
182 // Gateways declaring `system_subscription` — the only ones
183 // subscription_system_charge can ever auto-charge.
184 $systemChargeGateways = [];
185 foreach (GatewayManager::getInstance()->all() as $systemChargeGateway) {
186 if (!$systemChargeGateway->has('system_subscription') || $systemChargeGateway->isUpcoming()) {
187 continue;
188 }
189 $systemChargeGatewayMeta = $systemChargeGateway->getMeta();
190 $systemChargeGateways[] = [
191 'label' => Arr::get($systemChargeGatewayMeta, 'admin_title')
192 ?: Arr::get($systemChargeGatewayMeta, 'label')
193 ?: Arr::get($systemChargeGatewayMeta, 'title'),
194 'active' => $systemChargeGateway->isEnabled(),
195 ];
196 }
197
198 $fields = [
199 'setting_tabs' => [
200 'type' => 'section',
201 'disable_nesting' => true,
202 'default_tab' => 'store_setup',
203 'hide_tab_switch' => true,
204 'schema' => [
205 'store_setup' => [
206 'id' => '',
207 'title' => __('Store Setup', 'fluent-cart'),
208 'show_title' => false,
209 'type' => 'section',
210 'disable_nesting' => true,
211 'columns' => [
212 'default' => 1,
213 'md' => 1
214 ],
215 'schema' => [
216 'name_grid' => [
217 'type' => 'grid',
218 'columns' => [
219 'default' => 1,
220 'md' => 3
221 ],
222 'disable_nesting' => true,
223 'schema' => [
224 'label' => [
225 'type' => 'html',
226 'value' => '<span class="setting-label">' . __('Store Name', 'fluent-cart') . '</span>
227 <div class="form-note">' . __('Enter the public name of your online store.', 'fluent-cart') . '</div>'
228 ],
229 "store_name" => [
230 'wrapperClass' => 'col-span-2 flex items-center',
231 "label" => '',
232 "type" => "input",
233 "value" => "",
234 ],
235 ]
236 ],
237
238 'hr' => [
239 'type' => 'html',
240 'value' => '<hr class="settings-divider">'
241 ],
242
243
244 'logo_grid' => [
245 'type' => 'grid',
246 'columns' => [
247 'default' => 1,
248 'md' => 3
249 ],
250 'disable_nesting' => true,
251 'schema' => [
252 'label' => [
253 'type' => 'html',
254 'value' => '<span class="setting-label">' . __('Store Logo', 'fluent-cart') . '</span>
255 <div class="form-note">' . __("Upload your brand's logo. Recommended width: 512 pixels minimum.", 'fluent-cart') . '</div>'
256 ],
257 "store_logo" => [
258 "label" => false,
259 "type" => "media",
260 "value" => "",
261 'multiple' => false,
262 // if `condition_type` not specified then all condition type will be `and`
263
264 // 'conditions' => [
265 // [
266 // 'key' => 'store_name',
267 // 'operator' => '==',
268 // 'value' => [
269 // 'accessor' => 'order_mode'
270 // ]
271 // ],
272 // ],
273
274 ],
275 ]
276 ],
277
278 'hr2' => [
279 'type' => 'html',
280 'value' => '<hr class="settings-divider">'
281 ],
282
283 'mode_grid' => [
284 'type' => 'grid',
285 'columns' => [
286 'default' => 1,
287 'md' => 3
288 ],
289 'disable_nesting' => true,
290 'schema' => [
291 'label' => [
292 'type' => 'html',
293 'value' => '<span class="setting-label">' . __('Store Mode', 'fluent-cart') . '</span>
294 <div class="form-note">' . __("Select your store's operating mode: `Test` for setup, `Live` for real transactions.", 'fluent-cart') . '</div>'
295 ],
296 'order_mode' => [
297 'wrapperClass' => 'col-span-2 flex items-center',
298 "label" => '',
299 "type" => "radio",
300 "options" => [
301 [
302 "label" => __('Live', 'fluent-cart'),
303 "value" => 'live',
304 ],
305 [
306 "label" => __('Test', 'fluent-cart'),
307 "value" => 'test',
308 ],
309 ],
310 "value" => "live"
311 ],
312 ]
313 ],
314
315 'date_time_hr' => [
316 'type' => 'html',
317 'value' => '<hr class="settings-divider">'
318 ],
319
320 'address_grid' => [
321 'type' => 'grid',
322 'columns' => [
323 'default' => 1,
324 'md' => 3
325 ],
326 'disable_nesting' => true,
327 'schema' => [
328 'label' => [
329 'type' => 'html',
330 'value' => '<span class="setting-label">' . __('Store Address', 'fluent-cart') . '</span>
331 <div class="form-note">' . __("Provide your physical business address details.", 'fluent-cart') . '</div>'
332 ],
333 'address_input_grid' => [
334 'wrapperClass' => 'fct-compact-form',
335 'type' => 'grid',
336 'disable_nesting' => true,
337 'class' => 'col-span-2',
338 'schema' => [
339 'store_address_component' => [
340 'type' => 'component',
341 'component' => 'StoreSettings/AddressComponent',
342 'wrapperClass' => 'col-span-full'
343 ],
344 'store_address1' => [
345 'type' => 'hidden',
346 ],
347 'store_address2' => [
348 'type' => 'hidden',
349 ],
350 'store_city' => [
351 'type' => 'hidden',
352 ],
353 'store_postcode' => [
354 'type' => 'hidden',
355 ],
356 'store_country' => [
357 'type' => 'hidden',
358 ],
359 'store_state' => [
360 'type' => 'hidden',
361 ],
362 ]
363 ],
364 ]
365 ],
366
367
368 'settings_hr_2' => [
369 'type' => 'html',
370 'value' => '<hr class="settings-divider">'
371 ],
372
373 'business_details_grid' => [
374 'type' => 'grid',
375 'id' => 'business_details',
376 'columns' => [
377 'default' => 1,
378 'md' => 3
379 ],
380 'disable_nesting' => true,
381 'schema' => [
382 'label' => [
383 'type' => 'html',
384 'value' => '<span class="setting-label">' . __('Business Details', 'fluent-cart') . '</span>
385 <div class="form-note">' . __('Add your legal business identity details used for store records and compliance.', 'fluent-cart') . '</div>'
386 ],
387 'business_details_fields' => [
388 'type' => 'grid',
389 'columns' => [
390 'default' => 1,
391 'md' => 2
392 ],
393 'disable_nesting' => true,
394 'wrapperClass' => 'col-span-2',
395 'schema' => [
396 'company_name' => [
397 'label' => __('Company Name', 'fluent-cart'),
398 'type' => 'input',
399 'value' => '',
400 ],
401 'legal_registration_id' => [
402 'label' => __('Legal Registration ID', 'fluent-cart'),
403 'type' => 'input',
404 'value' => '',
405 ],
406 'seller_vat_id' => [
407 'label' => __('Seller VAT ID', 'fluent-cart'),
408 'type' => 'input',
409 'value' => '',
410 ],
411 'seller_tax_id' => [
412 'label' => __('Seller Tax ID', 'fluent-cart'),
413 'type' => 'input',
414 'value' => '',
415 ],
416 ]
417 ],
418 ]
419 ],
420
421 'settings_hr_3' => [
422 'type' => 'html',
423 'value' => '<hr class="settings-divider">'
424 ],
425
426 'currency_grid' => [
427 'type' => 'grid',
428 'columns' => [
429 'default' => 1,
430 'md' => 3
431 ],
432 'disable_nesting' => true,
433 'schema' => [
434 'label' => [
435 'type' => 'html',
436 'value' => '<span class="setting-label">' . __('Checkout Currency', 'fluent-cart') . '</span>
437 <div class="form-note">' . __("Select the primary currency for your store.", 'fluent-cart') . '</div>'
438 ],
439 "currency" => [
440 'wrapperClass' => 'col-span-2 flex items-center',
441 "label" => '',
442 "type" => "select",
443 'filterable' => true,
444 "options" => CurrencySettings::getFormattedCurrencies(),
445 "value" => "USD"
446 ],
447 ]
448 ],
449
450 'hr3' => [
451 'type' => 'html',
452 'value' => '<hr class="settings-divider">'
453 ],
454
455 'decimal_separator_grid' => [
456 'type' => 'grid',
457 'columns' => [
458 'default' => 1,
459 'md' => 3
460 ],
461 'disable_nesting' => true,
462 'schema' => [
463 'label' => [
464 'type' => 'html',
465 'value' => '<span class="setting-label">' . __('Number Format', 'fluent-cart') . '</span>
466 <div class="form-note">' . __("Select the character used to separate thousands or decimals in prices.", 'fluent-cart') . '</div>'
467 ],
468 'decimal_separator' => [
469 'wrapperClass' => 'col-span-2 flex items-start flex-col',
470 "label" => '',
471 "type" => "radio",
472 "options" => [
473 [
474 "label" => __('Comma & Dot (eg 10,000.00)', 'fluent-cart'),
475 "value" => 'dot'
476 ],
477 [
478 "label" => __('Dot & Comma (eg 10.000,00)', 'fluent-cart'),
479 "value" => 'comma'
480 ],
481 ],
482 "value" => "dot"
483 ],
484 ]
485 ],
486
487 'hr4' => [
488 'type' => 'html',
489 'value' => '<hr class="settings-divider">'
490 ],
491
492 'currency_position_grid' => [
493 'type' => 'grid',
494 'columns' => [
495 'default' => 1,
496 'md' => 3
497 ],
498 'disable_nesting' => true,
499 'schema' => [
500 'label' => [
501 'type' => 'html',
502 'value' => '<span class="setting-label">' . __('Currency Formatting', 'fluent-cart') . '</span>
503 <div class="form-note">' . __("Select how the currency should be formatted.", 'fluent-cart') . '</div>'
504 ],
505 'currency_position' => [
506 'wrapperClass' => 'col-span-2 flex items-center',
507 "label" => '',
508 "type" => "select",
509 "options" => [
510 [
511 "label" => __('Symbol before (eg: $100)', 'fluent-cart'),
512 "value" => 'before',
513 ],
514 [
515 "label" => __('Symbol after (eg: 100$)', 'fluent-cart'),
516 "value" => 'after',
517 ],
518 [
519 "label" => __('ISO before (eg: USD 100)', 'fluent-cart'),
520 "value" => 'iso_before',
521 ],
522 [
523 "label" => __('ISO after (eg: 100 USD)', 'fluent-cart'),
524 "value" => 'iso_after',
525 ],
526 [
527 "label" => __('Symbol & ISO (eg: $100 USD)', 'fluent-cart'),
528 "value" => 'symbool_before_iso',
529 ],
530 [
531 "label" => __('ISO & Symbol (eg: USD 100$)', 'fluent-cart'),
532 "value" => 'symbool_after_iso',
533 ],
534 [
535 "label" => __('ISO-Symbol (eg: USD $100)', 'fluent-cart'),
536 "value" => 'symbool_and_iso',
537 ],
538 ],
539 "value" => "before"
540 ],
541 ]
542 ],
543
544 'hr5' => [
545 'type' => 'html',
546 'value' => '<hr class="settings-divider">'
547 ],
548
549
550 'checkout_style_grid' => [
551 'type' => 'grid',
552 'columns' => [
553 'default' => 1,
554 'md' => 3
555 ],
556 'disable_nesting' => true,
557 'schema' => [
558 'label' => [
559 'type' => 'html',
560 'value' => '<span class="setting-label">' . __('Payment View', 'fluent-cart') . '</span>
561 <div class="form-note">' . __("Select how payment options are visually presented on checkout.", 'fluent-cart') . '</div>'
562 ],
563 "checkout_method_style" => [
564 'wrapperClass' => 'col-span-2 flex items-center',
565 "type" => "component",
566 'component' => 'PaymentView',
567 'label' => false,
568 "options" => [
569 [
570 "label" => '',
571 "value" => 'logo',
572 ],
573 [
574 "label" => __('Label Selector', 'fluent-cart'),
575 "value" => 'radio',
576 ],
577 ],
578 "value" => "logo"
579 ],
580 ]
581 ],
582
583 'settings_hr' => [
584 'type' => 'html',
585 'value' => '<hr class="settings-divider">'
586 ],
587
588 'date_time_format_grid' => [
589 'type' => 'grid',
590 'columns' => [
591 'default' => 1,
592 'md' => 3
593 ],
594 'disable_nesting' => true,
595 'schema' => [
596 'label' => [
597 'type' => 'html',
598 'value' => '<span class="setting-label">' . __('Date & Time Format', 'fluent-cart') . '</span>
599 <div class="form-note">' . __("Which date and time format to display. `Smart` keeps FluentCart's own format; `WordPress` follows Settings &rarr; General.", 'fluent-cart') . '</div>'
600 ],
601 'date_time_format_source' => [
602 'wrapperClass' => 'col-span-2 flex items-center',
603 'label' => '',
604 'type' => 'radio',
605 'options' => [
606 [
607 // Label only -- the stored value stays 'fluent_cart'.
608 'label' => __('Smart', 'fluent-cart'),
609 'value' => 'fluent_cart',
610 ],
611 [
612 'label' => __('WordPress', 'fluent-cart'),
613 'value' => 'wordpress',
614 ],
615 ],
616 'value' => 'fluent_cart'
617 ],
618 ]
619 ],
620
621 'timezone_source_grid' => [
622 'type' => 'grid',
623 'columns' => [
624 'default' => 1,
625 'md' => 3
626 ],
627 'disable_nesting' => true,
628 'schema' => [
629 'label' => [
630 'type' => 'html',
631 'value' => '<span class="setting-label">' . __('Timezone', 'fluent-cart') . '</span>
632 <div class="form-note">' . __("Which timezone to display dates in. `Browser` shows admin and dashboard dates in the viewer's own timezone, and renders emails and invoices in the timezone captured at checkout; `WordPress` uses the site timezone from Settings &rarr; General.", 'fluent-cart') . '</div>'
633 ],
634 'timezone_source' => [
635 'wrapperClass' => 'col-span-2 flex items-center',
636 'label' => '',
637 'type' => 'radio',
638 'options' => [
639 [
640 // Label only -- the stored value stays 'fluent_cart'.
641 'label' => __('Browser', 'fluent-cart'),
642 'value' => 'fluent_cart',
643 ],
644 [
645 'label' => __('WordPress', 'fluent-cart'),
646 'value' => 'wordpress',
647 ],
648 ],
649 'value' => 'fluent_cart'
650 ],
651 ]
652 ],
653
654 // 'settings_hr_modal' => [
655 // 'type' => 'html',
656 // 'value' => '<hr class="settings-divider">'
657 // ],
658 //
659 // 'modal_checkout_grid' => [
660 // 'type' => 'grid',
661 // 'columns' => [
662 // 'default' => 1,
663 // 'md' => 3
664 // ],
665 // 'disable_nesting' => true,
666 // 'schema' => [
667 // 'label' => [
668 // 'type' => 'html',
669 // 'value' => '<span class="setting-label">' . __('Buy Now Button Behavior', 'fluent-cart') . '</span>
670 // <div class="form-note">' . __("Choose how the Buy Now button behaves. Modal checkout provides a seamless experience without leaving the product page.", 'fluent-cart') . '</div>'
671 // ],
672 // "enable_modal_checkout" => [
673 // 'wrapperClass' => 'col-span-2 flex items-center',
674 // "label" => '',
675 // "type" => "radio",
676 // "options" => [
677 // [
678 // "label" => __('Redirect to Checkout Page', 'fluent-cart'),
679 // "value" => 'no',
680 // ],
681 // [
682 // "label" => __('Open Checkout in Modal', 'fluent-cart'),
683 // "value" => 'yes',
684 // ],
685 // ],
686 // "value" => "no"
687 // ],
688 // ]
689 // ],
690 ],
691 ],
692 // 'button_setup' => [
693 // 'title' => __('Button Setup', 'fluent-cart'),
694 // 'type' => 'tab-pane',
695 // 'disable_nesting' => true,
696 // 'schema' => [
697 // 'button_setup_settings' => [
698 // 'title' => __('Button Setup', 'fluent-cart'),
699 // 'type' => 'section',
700 // 'disable_nesting' => true,
701 // 'columns' => [
702 // 'default' => 1,
703 // 'md' => 2
704 // ],
705 // 'schema' => [
706 // "checkout_button_text" => [
707 // "label" => __('Checkout Button Text', 'fluent-cart'),
708 // "type" => "input",
709 // "value" => __('Buy now', 'fluent-cart')
710 // ],
711 // "cart_button_text" => [
712 // "label" => __('Cart Button Text', 'fluent-cart'),
713 // "type" => "input",
714 // "value" => __('Add to cart', 'fluent-cart')
715 // ],
716 //
717 // "popup_button_text" => [
718 // "label" => __('Popup Button Text', 'fluent-cart'),
719 // "type" => "input",
720 // "value" => __('View Product', 'fluent-cart')
721 // ],
722 // "out_of_stock_button_text" => [
723 // "label" => __('Out of Stock Button Text', 'fluent-cart'),
724 // "type" => "input",
725 // "value" => __('Out of stock', 'fluent-cart')
726 // ],
727 // "view_cart_button_text" => [
728 // "label" => __('View Cart Button Text', 'fluent-cart'),
729 // "type" => "input",
730 // "value" => __('View Cart', 'fluent-cart')
731 // ],
732 //
733 // "note_for_user_account_creation" => [
734 // "label" => __('Note for User Account Creation,', 'fluent-cart'),
735 // "type" => "input",
736 // "value" => __('You have subscription product in your cart, an account will be created', 'fluent-cart')
737 // ],
738 // ],
739 // ]
740 // ]
741 // ],
742 'pages_setup' => [
743 'id' => '',
744 'title' => __('Pages Setup', 'fluent-cart'),
745 'show_title' => false,
746 'type' => 'section',
747 'disable_nesting' => true,
748 'columns' => [
749 'default' => 1,
750 'md' => 1
751 ],
752 'schema' => [
753
754 'shop_page_grid' => [
755 'type' => 'grid',
756 'columns' => [
757 'default' => 1,
758 'md' => 3,
759 ],
760 'disable_nesting' => true,
761 'schema' => [
762 'label' => [
763 'type' => 'html',
764 'value' => '<span class="setting-label">' . __('Select Shop Page', 'fluent-cart') . '</span>
765 <div class="form-note">' . __("Select the page that showcases all of your products.", 'fluent-cart') . '</div>'
766 ],
767 'shop_page_id' => [
768 'wrapperClass' => 'col-span-2',
769 'page_title' => __('Shop', 'fluent-cart'),
770 'type' => 'component',
771 'component' => 'StoreSettings/PageSelector',
772 'page_key' => 'shop_page_id',
773 'preview_link' => $previewLinks['shop_page_id'],
774 'options' => $pages,
775 'hide_note' => true,
776 'value' => '',
777 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString(
778 '[fluent_cart_products]',
779 __('Products', 'fluent-cart')
780 ),
781 ],
782 ]
783 ],
784
785 'hr1' => [
786 'type' => 'html',
787 'value' => '<hr class="settings-divider">'
788 ],
789
790 'customer_profile_grid' => [
791 'type' => 'grid',
792 'columns' => [
793 'default' => 1,
794 'md' => 3
795 ],
796 'disable_nesting' => true,
797 'schema' => [
798 'label' => [
799 'type' => 'html',
800 'value' => '<span class="setting-label">' . __('Select Customer Profile Page', 'fluent-cart') . '</span>
801 <div class="form-note">' . __("Select the page where customers will manage their profile, orders, and downloads.", 'fluent-cart') . '</div>'
802 ],
803 'customer_profile_page_id' => [
804 'wrapperClass' => 'col-span-2',
805 'label' => false,
806 'page_title' => __('Account', 'fluent-cart'),
807 'type' => 'component',
808 'component' => 'StoreSettings/PageSelector',
809 'page_key' => 'customer_profile_page_id',
810 'preview_link' => $previewLinks['customer_profile_page_id'],
811 'hide_note' => true,
812 'options' => $pages,
813 'value' => '',
814 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString(
815 '[fluent_cart_customer_profile]',
816 __('Account', 'fluent-cart')
817 ),
818 ],
819 ]
820 ],
821
822 'hr2' => [
823 'type' => 'html',
824 'value' => '<hr class="settings-divider">'
825 ],
826
827 'cart_page_grid' => [
828 'type' => 'grid',
829 'columns' => [
830 'default' => 1,
831 'md' => 3
832 ],
833 'disable_nesting' => true,
834 'schema' => [
835 'label' => [
836 'type' => 'html',
837 'value' => '<span class="setting-label">' . __('Select Cart Page', 'fluent-cart') . '</span>
838 <div class="form-note">' . __("Select the page that will display customer's current shopping cart.", 'fluent-cart') . '</div>'
839 ],
840 'cart_page_id' => [
841 'wrapperClass' => 'col-span-2',
842 'label' => false,
843 'page_title' => __('Cart', 'fluent-cart'),
844 'type' => 'component',
845 'component' => 'StoreSettings/PageSelector',
846 'page_key' => 'cart_page_id',
847 'preview_link' => $previewLinks['cart_page_id'],
848 'hide_note' => true,
849 'options' => $pages,
850 'value' => '',
851 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString(
852 '[fluent_cart_cart]',
853 __('Cart', 'fluent-cart')
854 ),
855 ],
856 ]
857 ],
858
859 'hr3' => [
860 'type' => 'html',
861 'value' => '<hr class="settings-divider">'
862 ],
863
864 'receipt_page_grid' => [
865 'type' => 'grid',
866 'columns' => [
867 'default' => 1,
868 'md' => 3
869 ],
870 'disable_nesting' => true,
871 'schema' => [
872 'label' => [
873 'type' => 'html',
874 'value' => '<span class="setting-label">' . __('Select Receipt Page', 'fluent-cart') . '</span>
875 <div class="form-note">' . __("Select the page that will display order summary after a successful purchase.", 'fluent-cart') . '</div>'
876 ],
877 'receipt_page_id' => [
878 'wrapperClass' => 'col-span-2',
879 'label' => false,
880 'page_title' => __('Receipt', 'fluent-cart'),
881 'type' => 'component',
882 'component' => 'StoreSettings/PageSelector',
883 'page_key' => 'receipt_page_id',
884 'preview_link' => $previewLinks['receipt_page_id'],
885 'hide_note' => true,
886 'options' => $pages,
887 'value' => '',
888 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString(
889 '[fluent_cart_receipt]',
890 __('Receipt', 'fluent-cart')
891 ),
892 ],
893 ]
894 ],
895
896 'hr4' => [
897 'type' => 'html',
898 'value' => '<hr class="settings-divider">'
899 ],
900
901 'checkout_page_grid' => [
902 'type' => 'grid',
903 'columns' => [
904 'default' => 1,
905 'md' => 3
906 ],
907 'disable_nesting' => true,
908 'schema' => [
909 'label' => [
910 'type' => 'html',
911 'value' => '<span class="setting-label">' . __('Select Checkout Page', 'fluent-cart') . '</span>
912 <div class="form-note">' . __("Select the page where customers will finalize their purchase.", 'fluent-cart') . '</div>'
913 ],
914 'checkout_page_id' => [
915 'wrapperClass' => 'col-span-2',
916 'label' => false,
917 'page_title' => __('Checkout', 'fluent-cart'),
918 'type' => 'component',
919 'component' => 'StoreSettings/PageSelector',
920 'page_key' => 'checkout_page_id',
921 'preview_link' => $previewLinks['checkout_page_id'],
922 'hide_note' => true,
923 'options' => $pages,
924 'value' => '',
925 'note' => \FluentCart\App\Helpers\Helper::getShortcodeInstructionString(
926 '[fluent_cart_checkout]',
927 __('Checkout', 'fluent-cart')
928 ),
929 ],
930 ]
931 ]
932 ]
933 ],
934 'single_product_setup' => [
935 'title' => __('Product Page', 'fluent-cart'),
936 'show_title' => false,
937 'type' => 'section',
938 'disable_nesting' => true,
939 'columns' => [
940 'default' => 1,
941 'md' => 1
942 ],
943 'schema' => [
944 'product_settings_grid' => [
945 'type' => 'grid',
946 'columns' => [
947 'default' => 1,
948 'md' => 3
949 ],
950 'disable_nesting' => true,
951 'schema' => [
952 'label' => [
953 'type' => 'html',
954 'value' => '<span class="setting-label">' . __('Single Product Setup', 'fluent-cart') . '</span>
955 <div class="form-note">' . __("Control the display of relevant information.", 'fluent-cart') . '</div>'
956 ],
957 'fields' => [
958 'type' => 'grid',
959 'columns' => [
960 'default' => 1,
961 'md' => 1
962 ],
963 'disable_nesting' => true,
964 'schema' => [
965 "show_relevant_product_in_single_page" => [
966 "label" => __('Show Relevant In Single Page', 'fluent-cart'),
967 "type" => "checkbox",
968 "value" => "yes"
969 ],
970 "show_relevant_product_in_modal" => [
971 "label" => __('Show Relevant In Product Modal', 'fluent-cart'),
972 "type" => "checkbox",
973 "value" => "no"
974 ],
975 ]
976 ]
977
978 ]
979 ],
980 'hr' => [
981 'type' => 'html',
982 'value' => '<hr class="settings-divider">'
983 ],
984
985 'image_zoom_grid' => [
986 'type' => 'grid',
987 'columns' => [
988 'default' => 1,
989 'md' => 3
990 ],
991 'disable_nesting' => true,
992 'schema' => [
993 'label' => [
994 'type' => 'html',
995 'value' => '<span class="setting-label">' . __('Image Zooming', 'fluent-cart') . '</span>
996 <div class="form-note">' . __("Enable Image zoom in single product.", 'fluent-cart') . '</div>'
997 ],
998 'fields' => [
999 'type' => 'grid',
1000 'columns' => [
1001 'default' => 1,
1002 'md' => 1
1003 ],
1004 'disable_nesting' => true,
1005 'schema' => [
1006 "enable_image_zoom_in_single_product" => [
1007 "label" => __('Enable Zoom in Single Product', 'fluent-cart'),
1008 "type" => "checkbox",
1009 "value" => "no"
1010 ],
1011 "enable_image_zoom_in_modal" => [
1012 "label" => __('Enable Zoom in Modal', 'fluent-cart'),
1013 "type" => "checkbox",
1014 "value" => "no"
1015 ],
1016 ]
1017 ]
1018
1019 ]
1020 ],
1021
1022 'hr_image_zoom' => [
1023 'type' => 'html',
1024 'value' => '<hr class="settings-divider">'
1025 ],
1026
1027 'variation_grid' => [
1028 'type' => 'grid',
1029 'columns' => [
1030 'default' => 1,
1031 'md' => 3
1032 ],
1033 'disable_nesting' => true,
1034 'schema' => [
1035 'label' => [
1036 'type' => 'html',
1037 'value' => '<span class="setting-label">' . __('Variation View', 'fluent-cart') . '</span>
1038 <div class="form-note">' . __("Defines how product variations are visually presented to customers.", 'fluent-cart') . '</div>'
1039 ],
1040 "variation_view" => [
1041 'wrapperClass' => 'col-span-2 flex items-center',
1042 "label" => false,
1043 "type" => "select",
1044 "options" => [
1045 [
1046 "label" => __('Image', 'fluent-cart'),
1047 "value" => 'image',
1048 ],
1049 [
1050 "label" => __('Text', 'fluent-cart'),
1051 "value" => 'text',
1052 ],
1053 [
1054 "label" => __('Image with Text', 'fluent-cart'),
1055 "value" => 'both',
1056 ],
1057 ],
1058 "value" => ""
1059 ],
1060
1061 ]
1062 ],
1063 'hr2' => [
1064 'type' => 'html',
1065 'value' => '<hr class="settings-divider">'
1066 ],
1067 'variation_column_grid' => [
1068 'type' => 'grid',
1069 'columns' => [
1070 'default' => 1,
1071 'md' => 3
1072 ],
1073 'disable_nesting' => true,
1074 'schema' => [
1075 'label' => [
1076 'type' => 'html',
1077 'value' => '<span class="setting-label">' . __('Variation Columns', 'fluent-cart') . '</span>
1078 <div class="form-note">' . __("Set the column layout for how product variations are displayed within product sections.", 'fluent-cart') . '</div>'
1079 ],
1080 "variation_columns" => [
1081 'wrapperClass' => 'col-span-2 flex items-center',
1082 "label" => false,
1083 "type" => "select",
1084 "options" => [
1085 [
1086 "label" => __('One Column', 'fluent-cart'),
1087 "value" => 'one',
1088 ],
1089 [
1090 "label" => __('Two Columns', 'fluent-cart'),
1091 "value" => 'two',
1092 ],
1093 [
1094 "label" => __('Three Columns', 'fluent-cart'),
1095 "value" => 'three',
1096 ],
1097 [
1098 "label" => __('Four Columns', 'fluent-cart'),
1099 "value" => 'four',
1100 ],
1101 [
1102 "label" => __('Masonry', 'fluent-cart'),
1103 "value" => 'masonry',
1104 ],
1105 ],
1106 "value" => ""
1107 ],
1108
1109 ]
1110 ],
1111
1112 'hr3' => [
1113 'type' => 'html',
1114 'value' => '<hr class="settings-divider">'
1115 ],
1116 'product_slug_grid' => [
1117 'type' => 'grid',
1118 'columns' => [
1119 'default' => 1,
1120 'md' => 3
1121 ],
1122 'disable_nesting' => true,
1123 'schema' => [
1124 'label' => [
1125 'type' => 'html',
1126 'value' => '<span class="setting-label">' . __('Product Slug', 'fluent-cart') . '</span>
1127 <div class="form-note">' . __("Set Product Slug.", 'fluent-cart') . '</div>'
1128 ],
1129 "product_slug" => [
1130 'wrapperClass' => 'col-span-2 flex items-center',
1131 "label" => false,
1132 "type" => "input",
1133 "value" => ""
1134 ],
1135
1136 ]
1137 ],
1138 ],
1139 ],
1140 'compliance' => [
1141 'title' => __('Compliance', 'fluent-cart'),
1142 'show_title' => false,
1143 'type' => 'section',
1144 'disable_nesting' => true,
1145 'columns' => [
1146 'default' => 1,
1147 'md' => 1,
1148 ],
1149 'schema' => [
1150 'auto_login_after_account_creation' => [
1151 'wrapperClass' => 'fct-compliance-auto-login',
1152 'label' => __('Login after account creation', 'fluent-cart'),
1153 'type' => 'radio',
1154 'value' => 'no',
1155 'attributes' => [
1156 'aria-label' => __('Login after account creation', 'fluent-cart'),
1157 ],
1158 'options' => [
1159 [
1160 'label' => __("Don't auto login after account creation", 'fluent-cart'),
1161 'value' => 'no',
1162 ],
1163 [
1164 'label' => __('Enable auto login after account creation', 'fluent-cart'),
1165 'value' => 'yes',
1166 ],
1167 ],
1168 'note' => __('Choose whether customers are logged in automatically when FluentCart creates their account during registration or after checkout.', 'fluent-cart'),
1169 ],
1170 ],
1171 ],
1172 'cart_and_checkout' => [
1173 'title' => __('Cart & checkout', 'fluent-cart'),
1174 'show_title' => false,
1175 'type' => 'section',
1176 'disable_nesting' => true,
1177 'columns' => [
1178 'default' => 1,
1179 'md' => 1
1180 ],
1181 'schema' => [
1182 "show_cart_icon_in_body" => [
1183 "label" => __('Cart Icon In Body', 'fluent-cart'),
1184 "type" => "checkbox",
1185 "value" => "yes",
1186 'note' => sprintf(
1187 /* translators: %1$s: Display instruction text, %2$s: "Use this" text, %3$s: Copy to clipboard tooltip, %4$s: CSS class name, %5$s: CSS class description text */
1188 "<div class='pl-6'><p>%1\$s</p><p>%2\$s <code class='copyable-content' title='%3\$s'>%4\$s</code> %5\$s</p></div>",
1189 __('Display the cart icon in the body of the website', 'fluent-cart'),
1190 __('Use this', 'fluent-cart'),
1191 __('Copy to clipboard', 'fluent-cart'),
1192 'fcart-cart-toggle-button',
1193 __('CSS class to display the cart link/icon anywhere else.', 'fluent-cart')
1194 )
1195 ],
1196 'hr2' => [
1197 'type' => 'html',
1198 'value' => '<hr class="settings-divider">'
1199 ],
1200 "require_logged_in" => [
1201 "label" => __('Require user to be logged in for checkout (Coming soon)', 'fluent-cart'),
1202 "type" => "checkbox",
1203 "value" => "no",
1204 'note' => "<div class='pl-6'>" . __('Enforce that customers must be logged into their account to complete a purchase.', 'fluent-cart') . "</div>",
1205 'wrapperClass' => 'disabled'
1206 ],
1207 'hr3' => [
1208 'type' => 'html',
1209 'value' => '<hr class="settings-divider">'
1210 ],
1211
1212 'user_account_creation_mode_grid' => [
1213 'type' => 'grid',
1214 'columns' => [
1215 'default' => 1,
1216 'md' => 3
1217 ],
1218 'disable_nesting' => true,
1219 'schema' => [
1220 'label' => [
1221 'type' => 'html',
1222 'value' => '<span class="setting-label">' . __('User Account Creation Mode', 'fluent-cart') . '</span>
1223 <div class="form-note">' . __("User account will be created regardless of the mode if the order has a subscription.", 'fluent-cart') . '</div>'
1224 ],
1225 'user_account_input_grid' => [
1226 'type' => 'grid',
1227 'disable_nesting' => true,
1228 'class' => 'col-span-2',
1229 'schema' => [
1230 'user_account_creation_mode' => [
1231 'wrapperClass' => 'col-span-2 flex items-center',
1232 "label" => '',
1233 "type" => "radio",
1234 "options" => [
1235 [
1236 "label" => __('Create User Account Automatically after payment
1237 ', 'fluent-cart'),
1238 "value" => 'all',
1239 'note' => "<div class='my-[2px] form-note'>" . __('User account will be created automatically after payment.', 'fluent-cart') . "</div>",
1240 'option_class' => 'mb-2.5'
1241 ],
1242 [
1243 "label" => __('Give checkbox to create account on checkout page', 'fluent-cart'),
1244 "value" => 'user_choice',
1245 'note' => "<div class='my-[2px] form-note'>" . __('User will have an option to create an account during checkout.', 'fluent-cart') . "</div>",
1246 'option_class' => 'mb-2.5'
1247 ],
1248 [
1249 "label" => __('No need to create account for onetime purchases', 'fluent-cart'),
1250 "value" => 'only_subscription',
1251 'note' => "<div class='my-[2px] form-note'>" . __('User account will not be created for onetime purchases.', 'fluent-cart') . "</div>",
1252 ],
1253 ],
1254 "value" => "all"
1255 ],
1256 ]
1257 ],
1258 ]
1259 ],
1260
1261
1262 // "allow_checkout_signup" => [
1263 // "label" => __('Allow customers to create account during one-time checkout', 'fluent-cart'),
1264 // "type" => "checkbox",
1265 // "value" => "no",
1266 // 'note' => "<div class='pl-6'>" . __('Provide an option for customers to create a user account during a single, non-recurring checkout process.', 'fluent-cart') . "</div>"
1267 // ],
1268 // "force_ssl" => [
1269 // "label" => __('Force page to SSL (https)', 'fluent-cart'),
1270 // "type" => "checkbox",
1271 // "value" => "no"
1272 // ],
1273 'hr5' => [
1274 'type' => 'html',
1275 'value' => '<hr class="settings-divider">'
1276 ],
1277 "hide_coupon_field" => [
1278 "label" => __('Hide coupon field on checkout', 'fluent-cart'),
1279 "type" => "checkbox",
1280 "value" => "no",
1281 'note' => "<div class='pl-6'>" . __('Hide the coupon code input field on the checkout page.', 'fluent-cart') . "</div>"
1282 ],
1283 'hr6' => [
1284 'type' => 'html',
1285 'value' => '<hr class="settings-divider">'
1286 ],
1287 'order_invoice_grid' => [
1288 'type' => 'grid',
1289 'columns' => [
1290 'default' => 1,
1291 'md' => 3
1292 ],
1293 'disable_nesting' => true,
1294 'schema' => [
1295 'label' => [
1296 'type' => 'html',
1297 'value' => '<span class="setting-label">' . __('Receipt Settings', 'fluent-cart') . '</span>
1298 <div class="form-note">' . __("Setup your receipt.", 'fluent-cart') . '</div>'
1299 ],
1300 'invoice_input_grid' => [
1301 'type' => 'grid',
1302 'disable_nesting' => true,
1303 'class' => 'col-span-2',
1304 'schema' => [
1305 "min_receipt_number" => [
1306 'wrapperClass' => 'col-span-2 flex flex-col',
1307 "label" => __('Minimum Receipt Number', 'fluent-cart'),
1308 "type" => "input",
1309 "value" => "",
1310 'note' => sprintf(
1311 "<div class=''>%s</div>",
1312 sprintf(
1313 /* translators: %s is the next receipt number */
1314 __('Next Receipt Number: %s', 'fluent-cart'),
1315 OrderService::getNextReceiptNumber()
1316 )
1317 ),
1318
1319 ],
1320 "inv_prefix" => [
1321 'wrapperClass' => 'col-span-2 flex flex-col',
1322 "label" => __('Receipt Prefix', 'fluent-cart'),
1323 "type" => "input",
1324 "value" => "",
1325 ],
1326 ]
1327 ],
1328 ]
1329 ],
1330 ]
1331 ],
1332 'subscriptions_setup' => [
1333 'title' => __('Subscriptions', 'fluent-cart'),
1334 'show_title' => false,
1335 'type' => 'section',
1336 'disable_nesting' => true,
1337 'columns' => [
1338 'default' => 1,
1339 'md' => 1
1340 ],
1341 'schema' => [
1342 'subscription_early_payment_grid' => [
1343 'type' => 'grid',
1344 'wrapperClass' => 'items-start mb-6',
1345 'columns' => [
1346 'default' => 1,
1347 'md' => 3
1348 ],
1349 'disable_nesting' => true,
1350 'schema' => [
1351 'label' => [
1352 'type' => 'html',
1353 'value' => '<span class="setting-label">' . __('Early Payment', 'fluent-cart') . (!$isProActive ? ' <img src="' . esc_url($proFeatureIcon) . '" alt="' . esc_attr__('Pro feature', 'fluent-cart') . '" class="pro-feature-icon" style="margin-left: 6px; width: 14px; height: 14px; display: inline-block; vertical-align: text-top;" />' : '') . '</span>
1354 <div class="form-note">' . __('Let customers pay remaining installments before their due date.', 'fluent-cart') . '</div>'
1355 ],
1356 'fields' => [
1357 'type' => 'grid',
1358 'columns' => [
1359 'default' => 1,
1360 'md' => 1
1361 ],
1362 'disable_nesting' => true,
1363 'class' => 'col-span-2',
1364 'schema' => [
1365 'enable_early_payment_for_installment' => [
1366 'label' => __('Enable early payment for installment', 'fluent-cart'),
1367 'type' => 'checkbox',
1368 'value' => 'yes',
1369 'disabled' => !$isProActive,
1370 'wrapperClass' => !$isProActive ? 'disabled' : '',
1371 'note' => !$isProActive
1372 ? "<div class='pl-6'>" . __('This is a FluentCart Pro feature.', 'fluent-cart') . "</div>"
1373 : "<div class='pl-6'>" . __('Allow customers to pay remaining installments early from subscription screens.', 'fluent-cart') . "</div>"
1374 ]
1375 ]
1376 ]
1377 ]
1378 ],
1379 'subscription_management_mode_grid' => [
1380 'type' => 'grid',
1381 'wrapperClass' => 'items-start',
1382 'columns' => [
1383 'default' => 1,
1384 'md' => 3
1385 ],
1386 'disable_nesting' => true,
1387 'schema' => [
1388 'label' => [
1389 'type' => 'html',
1390 'value' => sprintf(
1391 /* translators: 1: setting label, 2: setting description */
1392 '<span class="setting-label">%1$s</span>
1393 <div class="form-note">%2$s</div>',
1394 __('Renewal Billing', 'fluent-cart'),
1395 __('Choose how recurring subscription payments are collected.', 'fluent-cart')
1396 )
1397 ],
1398 'fields' => [
1399 'type' => 'grid',
1400 'columns' => [
1401 'default' => 1,
1402 'md' => 1
1403 ],
1404 'disable_nesting' => true,
1405 'class' => 'col-span-2',
1406 'schema' => [
1407 // Compact, guarded control: shows only the CURRENT mode with a
1408 // short how-it-works summary and a Change button. Editing goes
1409 // through a disclaimer confirm + dialog so the store-wide
1410 // billing decision can never be flipped by a stray click.
1411 'subscription_management_mode' => [
1412 'label' => false,
1413 'component' => 'StoreSettings/SubscriptionModeManager',
1414 'disable_nesting' => true,
1415 'value' => 'gateway_managed',
1416 // Read-only renewal-order schedule, built live from the
1417 // same map the scheduler uses (filterable).
1418 'schedule_items' => $invoiceScheduleList,
1419 // Which gateways auto-charge is actually possible on.
1420 'system_charge_gateways' => $systemChargeGateways,
1421 ],
1422 // Keep these in the form payload — the component above writes
1423 // them; without a schema entry the keys would drop out of the
1424 // saved values.
1425 'subscription_system_charge' => [
1426 'type' => 'hidden',
1427 'value' => 'no',
1428 ],
1429 'subscription_manual_fallback' => [
1430 'type' => 'hidden',
1431 'value' => 'no',
1432 ],
1433 ]
1434 ]
1435 ]
1436 ],
1437 'subscription_mode_guard_grid' => [
1438 'type' => 'grid',
1439 'wrapperClass' => 'items-start mt-6',
1440 'columns' => [
1441 'default' => 1,
1442 'md' => 3
1443 ],
1444 'disable_nesting' => true,
1445 'schema' => [
1446 'label' => [
1447 'type' => 'html',
1448 'value' => sprintf(
1449 /* translators: 1: setting label, 2: setting description */
1450 '<span class="setting-label">%1$s</span>
1451 <div class="form-note">%2$s</div>',
1452 __('Staging Protection', 'fluent-cart'),
1453 __('Prevent staging or test copies of your store from billing real customers.', 'fluent-cart')
1454 )
1455 ],
1456 'fields' => [
1457 'type' => 'grid',
1458 'columns' => [
1459 'default' => 1,
1460 'md' => 1
1461 ],
1462 'disable_nesting' => true,
1463 'class' => 'col-span-2',
1464 'schema' => [
1465 'subscription_mode_guard' => [
1466 'label' => __('Don\'t bill live subscriptions from this site while it is in test mode', 'fluent-cart'),
1467 'type' => 'checkbox',
1468 'value' => 'yes',
1469 'note' => sprintf(
1470 /* translators: 1: the setting's explanatory note text */
1471 "<div class='pl-6'>%1\$s</div>",
1472 __('In test mode, this site won\'t invoice, charge, or email live subscriptions — so a staging copy can never double-bill customers. Billing from this site resumes when it is live again.', 'fluent-cart')
1473 )
1474 ],
1475 ]
1476 ]
1477 ]
1478 ],
1479 ]
1480 ],
1481 'appearance' => $this->getAppearanceSchema(),
1482 ]
1483 ],
1484 ];
1485
1486
1487 // Only show weight/dimension unit fields to users with shipping-sensitive permission
1488 if (PermissionManager::hasPermission(['store/sensitive'])) {
1489 $storeSchema = &$fields['setting_tabs']['schema']['store_setup']['schema'];
1490
1491 $storeSchema['weight_unit_grid_divider'] = [
1492 'type' => 'html',
1493 'value' => '<hr class="settings-divider">'
1494 ];
1495
1496 $storeSchema['weight_unit_grid'] = [
1497 'type' => 'grid',
1498 'columns' => ['default' => 1, 'md' => 3],
1499 'disable_nesting' => true,
1500 'schema' => [
1501 'label' => [
1502 'type' => 'html',
1503 'value' => '<span class="setting-label">' . __('Weight Unit', 'fluent-cart') . '</span>
1504 <div class="form-note">' . __('Unit used for product weight measurements.', 'fluent-cart') . '</div>'
1505 ],
1506 'weight_unit' => [
1507 'wrapperClass' => 'col-span-2 flex items-center',
1508 'label' => '',
1509 'type' => 'select',
1510 'options' => [
1511 ['label' => __('kg', 'fluent-cart'), 'value' => 'kg'],
1512 ['label' => __('g', 'fluent-cart'), 'value' => 'g'],
1513 ['label' => __('lbs', 'fluent-cart'), 'value' => 'lbs'],
1514 ['label' => __('oz', 'fluent-cart'), 'value' => 'oz'],
1515 ],
1516 'value' => 'kg'
1517 ],
1518 ]
1519 ];
1520 $storeSchema['dimension_unit_grid'] = [
1521 'type' => 'grid',
1522 'columns' => ['default' => 1, 'md' => 3],
1523 'disable_nesting' => true,
1524 'schema' => [
1525 'label' => [
1526 'type' => 'html',
1527 'value' => '<span class="setting-label">' . __('Dimension Unit', 'fluent-cart') . '</span>
1528 <div class="form-note">' . __('Unit used for product dimension measurements.', 'fluent-cart') . '</div>'
1529 ],
1530 'dimension_unit' => [
1531 'wrapperClass' => 'col-span-2 flex items-center',
1532 'label' => '',
1533 'type' => 'select',
1534 'options' => [
1535 ['label' => __('cm', 'fluent-cart'), 'value' => 'cm'],
1536 ['label' => __('mm', 'fluent-cart'), 'value' => 'mm'],
1537 ['label' => __('in', 'fluent-cart'), 'value' => 'in'],
1538 ['label' => __('m', 'fluent-cart'), 'value' => 'm'],
1539 ],
1540 'value' => 'cm'
1541 ],
1542 ]
1543 ];
1544 }
1545
1546 return apply_filters("fluent_cart/store_settings/fields", $fields, []);
1547 }
1548
1549 public function isModuleTabEnabled(): bool
1550 {
1551 $fields = $this->fields();
1552 return isset($fields['setting_tabs']['schema']['modules_tab']);
1553 }
1554
1555 /**
1556 * @param string|null $key like stripe or paypal
1557 * All store settings if key is not provided
1558 * @return mixed
1559 *
1560 */
1561
1562 public function get($key = null, $default = null)
1563 {
1564 if (empty($key)) {
1565 return $this->storeSettings;
1566 }
1567
1568 if (is_array($key)) {
1569 $data = Arr::only($this->storeSettings, $key);
1570 } else {
1571 $data = Arr::get($this->storeSettings, $key);
1572 }
1573
1574 return empty($data) ? $default : $data;
1575 }
1576
1577 public function moduleSettings(): array
1578 {
1579 $settings = $this->get('modules_settings', []);
1580 return Arr::wrap($settings);
1581 }
1582
1583 /**
1584 * @param array $settings like Stripe or PayPal settings array
1585 * Save store settings
1586 * @return array
1587 *
1588 */
1589 public function save(array $settings)
1590 {
1591 $prevSettings = get_option($this->optionKey, []);
1592 $prevSettings = wp_parse_args($prevSettings, $this->getDefaultSettings());
1593
1594 $settings = wp_parse_args($settings, $prevSettings);
1595
1596 $customerProfilePageId = Arr::get($settings, 'customer_profile_page_id', 0);
1597 if ($customerProfilePageId) {
1598 $settings['customer_profile_page_slug'] = $this->getCustomerDashboardPageSlug($customerProfilePageId, false);
1599 }
1600
1601 update_option($this->optionKey, $settings, true);
1602 $this->storeSettings = $settings;
1603 self::$cachedStoreSettings = $this->storeSettings;
1604 wp_cache_delete(self::CACHE_KEY, self::CACHE_GROUP);
1605
1606 $isSlugChanged = Arr::get($prevSettings, 'product_slug') !== Arr::get($settings, 'product_slug');
1607 $isAccountPageChanged = Arr::get($prevSettings, 'customer_profile_page_slug') !== Arr::get($settings, 'customer_profile_page_slug');
1608
1609 if ($isSlugChanged || $isAccountPageChanged) {
1610 flush_rewrite_rules();
1611 delete_option('rewrite_rules');
1612 }
1613
1614 return $this->storeSettings;
1615 }
1616
1617 public function set(string $key, $value)
1618 {
1619 return $this->save([
1620 $key => $value
1621 ]);
1622 }
1623
1624 public function getCurrency()
1625 {
1626 return $this->get('currency', 'USD');
1627 }
1628
1629 public function getCurrencySymbol()
1630 {
1631 $currency = $this->getCurrency();
1632 return CurrenciesHelper::getCurrencySign($currency);
1633 }
1634
1635 public function isCheckoutPage(): bool
1636 {
1637 global $post;
1638 if (!$post instanceof \WP_Post) {
1639 return false;
1640 }
1641 $pageId = $this->getCheckoutPageId();
1642 return intval($pageId) === intval($post->ID);
1643 }
1644
1645 public function getCheckoutPageId()
1646 {
1647 return Arr::get($this->storeSettings, 'checkout_page_id');
1648 }
1649
1650 public function getPagesSettings(): array
1651 {
1652 return Arr::only(
1653 $this->storeSettings,
1654 [
1655 'checkout_page_id',
1656 'custom_payment_page_id',
1657 'registration_page_id',
1658 'login_page_id',
1659 'cart_page_id',
1660 'receipt_page_id',
1661 'shop_page_id',
1662 'customer_profile_page_id',
1663 'customer_profile_page_slug',
1664 ]
1665 );
1666 }
1667
1668 public function getCheckoutPage(): string
1669 {
1670 if ($pageID = $this->getCheckoutPageId()) {
1671 return $this->getPageLink($pageID);
1672 }
1673
1674 return '';
1675 }
1676
1677
1678 public function getCustomerProfilePageId()
1679 {
1680 return Arr::get($this->storeSettings, 'customer_profile_page_id');
1681 }
1682
1683 public function getCustomerProfilePage(): string
1684 {
1685 if ($pageId = $this->getCustomerProfilePageId()) {
1686 return $this->getPageLink($pageId);
1687 }
1688 return '';
1689 }
1690
1691
1692 public function getCartPageId()
1693 {
1694 return Arr::get($this->storeSettings, 'cart_page_id');
1695 }
1696
1697 public function getCartPage(): string
1698 {
1699 if ($pageId = $this->getCartPageId()) {
1700 if (Pages::isPage($pageId)) {
1701 return $this->getPageLink($pageId);
1702 }
1703 }
1704 return '';
1705 }
1706
1707 public function getShopPageId()
1708 {
1709 return Arr::get($this->storeSettings, 'shop_page_id');
1710 }
1711
1712 public function getShopPage(): string
1713 {
1714 if ($pageId = $this->getShopPageId()) {
1715 if (Pages::isPage($pageId)) {
1716 return $this->getPageLink($pageId);
1717 }
1718 }
1719
1720 return '';
1721 }
1722
1723 public function getReceiptPageId()
1724 {
1725 return Arr::get($this->storeSettings, 'receipt_page_id');
1726 }
1727
1728 public function getReceiptPage(): string
1729 {
1730 if ($pageId = $this->getReceiptPageId()) {
1731 if (Pages::isPage($pageId)) {
1732 return $this->getPageLink($pageId);
1733 }
1734 }
1735 return home_url();
1736 }
1737
1738
1739 public function getBuyButtonText(): string
1740 {
1741 return esc_html(Arr::get($this->storeSettings, 'checkout_button_text', __('Buy now', 'fluent-cart')));
1742 }
1743
1744 public function getViewCartButtonText(): string
1745 {
1746 return esc_html(Arr::get($this->storeSettings, 'view_cart_button_text', __('View Cart', 'fluent-cart')));
1747 }
1748
1749 /**
1750 * @return string
1751 *
1752 * Get cart button text
1753 */
1754 public function getCartButtonText(): string
1755 {
1756 return esc_html(Arr::get($this->storeSettings, 'cart_button_text', __('Add To Cart', 'fluent-cart')));
1757 }
1758
1759 public function toArray(): array
1760 {
1761 return $this->storeSettings;
1762 }
1763
1764 /**
1765 * @return string
1766 *
1767 * Get the base address1 for the store.
1768 */
1769 public function getBaseAddressLine1()
1770 {
1771 return Arr::get($this->storeSettings, 'store_address1');
1772 }
1773
1774 public function getFormattedFullAddress()
1775 {
1776 $addressParts = [
1777 trim(Arr::get($this->storeSettings, 'store_address1') ?? ''),
1778 trim(Arr::get($this->storeSettings, 'store_address2') ?? ''),
1779 trim(Arr::get($this->storeSettings, 'store_city') ?? ''),
1780 trim(AddressHelper::getStateNameByCode(
1781 Arr::get($this->storeSettings, 'store_state'),
1782 Arr::get($this->storeSettings, 'store_country')
1783 )),
1784 trim(Arr::get($this->storeSettings, 'store_postcode') ?? ''),
1785 trim(AddressHelper::getCountryNameByCode(Arr::get($this->storeSettings, 'store_country')))
1786 ];
1787
1788 // Filter out empty or null parts
1789 $addressParts = array_filter($addressParts, function ($part) {
1790 return $part !== '';
1791 });
1792
1793 // Join parts with comma and space
1794 $addressString = implode(', ', $addressParts);
1795 return $addressString;
1796 }
1797
1798 /**
1799 * @return string
1800 *
1801 * Get the base address2 for the store.
1802 */
1803 public function getBaseAddressLine2()
1804 {
1805 return Arr::get($this->storeSettings, 'store_address2');
1806 }
1807
1808 /**
1809 * @return string
1810 *
1811 * Get the base country for the store.
1812 */
1813 public function getBaseCountry()
1814 {
1815 return Arr::get($this->storeSettings, 'store_country');
1816 }
1817
1818 /**
1819 * @return string
1820 *
1821 * Get the base state for the store.
1822 */
1823 public function getBaseState()
1824 {
1825 return Arr::get($this->storeSettings, 'store_state');
1826 }
1827
1828 /**
1829 * @return string
1830 *
1831 * Get the base city for the store.
1832 */
1833 public function getBaseCity()
1834 {
1835 return Arr::get($this->storeSettings, 'store_city');
1836 }
1837
1838 /**
1839 * @return string
1840 *
1841 * Get the base postcode for the store.
1842 */
1843 public function getBasePostcode()
1844 {
1845 return Arr::get($this->storeSettings, 'store_postcode');
1846 }
1847
1848 public function getInvoicePrefix()
1849 {
1850 // @todo: make this dynamic from settings
1851 return $this->get('inv_prefix', '');
1852 }
1853
1854 public function getInvoiceSuffix(): string
1855 {
1856 return '';
1857 }
1858
1859 public function getCustomerDashboardPageSlug($pageId = null, $cached = true)
1860 {
1861 $slug = Arr::get($this->storeSettings, 'customer_profile_page_slug', '');
1862
1863 if ($cached && $slug) {
1864 return $slug;
1865 }
1866
1867 if (!$pageId) {
1868 $pageId = Arr::get($this->storeSettings, 'customer_profile_page_id');
1869 }
1870
1871 if (!$pageId || !get_post($pageId)) {
1872 return $slug;
1873 }
1874
1875 $url = get_page_link($pageId);
1876 $url = rtrim($url, '/');
1877
1878 if (!$url) {
1879 return $slug;
1880 }
1881
1882 return trim(str_replace(home_url('/'), '', $url), '/');
1883 }
1884
1885 /**
1886 * The Appearance settings tab.
1887 *
1888 * Built from the ColorPalette registry rather than written out by hand, so
1889 * a colour added to the registry becomes settable here, sanitised on save
1890 * and written to the storefront without three separate edits.
1891 *
1892 * @return array
1893 */
1894 protected function getAppearanceSchema(): array
1895 {
1896 // Plain text — the component renders these as text nodes, so no markup
1897 // and no escaping here or the entities would show up verbatim.
1898 $sourceOptions = [
1899 [
1900 'label' => __("FluentCart's own colors", 'fluent-cart'),
1901 'value' => ColorPalette::SOURCE_DEFAULT,
1902 'icon' => 'PaletteLine',
1903 'note' => __('The storefront keeps the colors it ships with.', 'fluent-cart'),
1904 ],
1905 [
1906 'label' => __('Inherit from the active theme', 'fluent-cart'),
1907 'value' => ColorPalette::SOURCE_THEME,
1908 'icon' => 'PaintLine',
1909 'note' => sprintf(
1910 '%1$s %2$s',
1911 __('The storefront palette is rebuilt from your theme, and follows it when the theme changes.', 'fluent-cart'),
1912 ThemePalette::sourceLabel()
1913 ),
1914 ],
1915 [
1916 'label' => __('Customize', 'fluent-cart'),
1917 'value' => ColorPalette::SOURCE_CUSTOM,
1918 'icon' => 'PaletteLine',
1919 'note' => __('Pick the colors yourself. Only the ones you set are written to the storefront.', 'fluent-cart'),
1920 ],
1921 ];
1922
1923 $sourceHeading = [
1924 'label' => __('Where colors come from', 'fluent-cart'),
1925 'note' => __('Storefront colors cascade from a small set of globals, so changing one here updates every page that uses it.', 'fluent-cart'),
1926 ];
1927
1928 return [
1929 'title' => __('Appearance', 'fluent-cart'),
1930 'show_title' => false,
1931 'type' => 'section',
1932 'wrapperClass' => 'fct-appearance-component-section',
1933 'disable_nesting' => true,
1934 'columns' => [
1935 'default' => 1,
1936 'md' => 1,
1937 ],
1938 'schema' => [
1939 'appearance_component' => [
1940 'type' => 'component',
1941 'component' => 'StoreSettings/AppearanceComponent',
1942 'wrapperClass' => 'col-span-full',
1943 'label' => false,
1944 // Rendered by the component itself, so the heading sits
1945 // inside .fct-appearance-component with the controls it
1946 // describes rather than as a sibling html field.
1947 'heading' => $sourceHeading,
1948 'source_options' => $sourceOptions,
1949 'custom_source' => ColorPalette::SOURCE_CUSTOM,
1950 'color_groups' => $this->getAppearanceColorGroups(),
1951 'preview' => $this->getAppearancePreviewData(),
1952 ],
1953 'appearance_source' => [
1954 'type' => 'hidden',
1955 'value' => ColorPalette::SOURCE_DEFAULT,
1956 ],
1957 'appearance_colors' => [
1958 'type' => 'hidden',
1959 'value' => (object)[],
1960 ],
1961 ],
1962 ];
1963 }
1964
1965 /**
1966 * What the storefront preview needs to paint itself.
1967 *
1968 * The preview is drawn from semantic roles rather than settings keys, so
1969 * one mock card covers all three sources: `customize` reads the pickers the
1970 * owner has filled in, and the other two are resolved here because their
1971 * colors only exist server side — `inherit_from_theme` is mixed out of
1972 * theme.json by ThemePalette and nothing about it reaches the browser.
1973 *
1974 * @return array
1975 */
1976 protected function getAppearancePreviewData(): array
1977 {
1978 // No colour is sent with a slot. appearance.scss already writes every
1979 // preview surface as `var(--fct-pv-x, <fallback>)`, so the mock has the
1980 // same single source of truth the storefront does: a slot the component
1981 // cannot resolve simply goes undeclared and the stylesheet's fallback
1982 // applies.
1983 return [
1984 'slots' => $this->getAppearancePreviewSlots(),
1985 'theme_source' => ColorPalette::SOURCE_THEME,
1986 'theme_roles' => $this->getAppearanceThemeRoles(),
1987 'notes' => [
1988 ColorPalette::SOURCE_THEME => $this->getAppearanceThemeNote(),
1989 ColorPalette::SOURCE_CUSTOM => __('Set only the colors you want to change — anything you leave unset keeps FluentCart\'s own default.', 'fluent-cart'),
1990 ],
1991 ];
1992 }
1993
1994 /**
1995 * Which custom property the preview paints each surface with.
1996 *
1997 * Keyed by settings key, not by role. Four separate globals claim the
1998 * `accent` role — active text, the brand background and both active
1999 * borders — so collapsing the preview to roles would let one picker drive
2000 * surfaces the storefront keeps apart, and leave the others with no visible
2001 * effect at all. `role` is carried only for theme inheritance, which
2002 * genuinely is role-based: FrontendTheme::getThemeColors() gives every key
2003 * sharing a role the same colour, so the preview collapsing there is the
2004 * storefront's own behaviour rather than a shortcut.
2005 *
2006 * A slot with no `key` is a surface no global controls; it follows the
2007 * role's default and cannot be edited.
2008 *
2009 * @return array
2010 */
2011 protected function getAppearancePreviewSlots(): array
2012 {
2013 return [
2014 ['var' => 'surface', 'role' => 'surface', 'key' => 'card_bg_color'],
2015 ['var' => 'surface-mute', 'role' => 'surface_mute'],
2016 ['var' => 'surface-alt', 'role' => 'surface_alt', 'key' => 'secondary_bg_color'],
2017 ['var' => 'text', 'role' => 'text', 'key' => 'primary_text_color'],
2018 ['var' => 'text-muted', 'role' => 'text_muted', 'key' => 'secondary_text_color'],
2019 ['var' => 'text-placeholder', 'role' => 'text_placeholder', 'key' => 'input_placeholder_text_color'],
2020 ['var' => 'input-bg', 'role' => 'surface', 'key' => 'input_bg_color'],
2021 ['var' => 'input-text', 'role' => 'text', 'key' => 'input_text_color'],
2022 ['var' => 'input-disabled-bg', 'role' => 'surface_mute', 'key' => 'input_disabled_bg_color'],
2023 ['var' => 'accent-text', 'role' => 'accent', 'key' => 'primary_active_text_color'],
2024 ['var' => 'active-border', 'role' => 'accent', 'key' => 'active_border_color'],
2025 ['var' => 'border', 'role' => 'border', 'key' => 'border_color'],
2026 ['var' => 'divider', 'role' => 'divider', 'key' => 'divider_color'],
2027 ['var' => 'button-bg', 'role' => 'button_bg', 'key' => 'btn_bg_color'],
2028 ['var' => 'button-text', 'role' => 'button_text', 'key' => 'btn_text_color'],
2029 ['var' => 'secondary-button-bg', 'role' => 'surface', 'key' => 'secondary_btn_bg_color'],
2030 ['var' => 'secondary-button-text', 'role' => 'text', 'key' => 'secondary_btn_text_color'],
2031 ['var' => 'secondary-button-border', 'role' => 'border', 'key' => 'secondary_btn_border_color'],
2032 ['var' => 'secondary-button-hover-bg', 'role' => 'surface_mute', 'key' => 'secondary_btn_hover_bg_color'],
2033 ];
2034 }
2035
2036 /**
2037 * Why the theme preview may not be the whole truth.
2038 *
2039 * Themes such as Astra publish their palette as `var(--ast-global-color-0)`
2040 * rather than as colors. FrontendTheme writes those references straight
2041 * through and the browser resolves them on the storefront, but the property
2042 * is not declared in wp-admin, so the preview cannot show them. Saying so
2043 * beats showing FluentCart's colors and letting the owner believe that is
2044 * what inheriting will look like.
2045 *
2046 * @return string Empty when the preview is faithful.
2047 */
2048 protected function getAppearanceThemeNote(): string
2049 {
2050 if (!ThemePalette::hasUsableSource()) {
2051 return __('The active theme publishes nothing to inherit, so the storefront keeps FluentCart\'s own colors.', 'fluent-cart');
2052 }
2053
2054 foreach (ThemePalette::resolve() as $value) {
2055 if (!sanitize_hex_color((string)$value)) {
2056 return __('This theme publishes its palette as CSS variables. The storefront reads them, but they cannot be resolved here — the preview shows FluentCart\'s colors in their place.', 'fluent-cart');
2057 }
2058 }
2059
2060 return '';
2061 }
2062
2063 /**
2064 * The theme's colors for the preview, role by role.
2065 *
2066 * Only roles that resolved to a real color are returned — the preview
2067 * falls back to each slot's own default for the rest.
2068 *
2069 * @return array Role => hex.
2070 */
2071 protected function getAppearanceThemeRoles(): array
2072 {
2073 // With nothing to inherit, FrontendTheme writes no declarations at all,
2074 // so the storefront keeps FluentCart's colors — and so must the
2075 // preview, or it would promise a change that never happens.
2076 if (!ThemePalette::hasUsableSource()) {
2077 return [];
2078 }
2079
2080 $roles = [];
2081
2082 foreach (ThemePalette::resolve() as $role => $value) {
2083 // A theme that publishes `var(--x)` is written to the storefront
2084 // verbatim and resolved by the browser there, but that property is
2085 // not declared in wp-admin. Unpreviewable, so leave it out and let
2086 // the slot fall back to its own default.
2087 $hex = sanitize_hex_color((string)$value);
2088
2089 if ($hex) {
2090 $roles[$role] = $hex;
2091 }
2092 }
2093
2094 return $roles;
2095 }
2096
2097 /**
2098 * The colour knobs, grouped the way the registry groups them, as data the
2099 * appearance component renders its pickers from.
2100 *
2101 * @return array
2102 */
2103 protected function getAppearanceColorGroups(): array
2104 {
2105 $groups = [];
2106
2107 foreach (ColorPalette::groups() as $groupKey => $groupLabel) {
2108 $globals = ColorPalette::globalsFor($groupKey);
2109
2110 if (!$globals) {
2111 continue;
2112 }
2113
2114 $fields = [];
2115
2116 foreach ($globals as $key => $definition) {
2117 $fields[] = [
2118 'key' => $key,
2119 'label' => Arr::get($definition, 'label', $key),
2120 'note' => $this->getAppearanceFieldNote($definition),
2121 'default' => (string)Arr::get($definition, 'default', ''),
2122 ];
2123 }
2124
2125 $groups[] = [
2126 'key' => $groupKey,
2127 'label' => $groupLabel,
2128 'fields' => $fields,
2129 ];
2130 }
2131
2132 return $groups;
2133 }
2134
2135 /**
2136 * The hint under one colour picker: what it drives, and the value it
2137 * falls back to when left empty.
2138 *
2139 * @param array $definition
2140 * @return string
2141 */
2142 protected function getAppearanceFieldNote(array $definition): string
2143 {
2144 $usage = (string)Arr::get($definition, 'note', '');
2145 $default = (string)Arr::get($definition, 'default', '');
2146
2147 if ($default === '') {
2148 return $usage;
2149 }
2150
2151 if ($usage === '') {
2152 /* translators: %1$s: the hex colour FluentCart falls back to */
2153 return sprintf(__('Default: %1$s', 'fluent-cart'), $default);
2154 }
2155
2156 /* translators: 1: what the colour is used for, 2: the hex colour FluentCart falls back to */
2157 return sprintf(__('%1$s Default: %2$s', 'fluent-cart'), $usage, $default);
2158 }
2159
2160 /**
2161 * Get theme colors as CSS variable string.
2162 *
2163 * @return string
2164 */
2165 public function getThemeColors(): string
2166 {
2167 $themeSetup = $this->get('theme_setup');
2168 $colors = '';
2169
2170 if (!empty($themeSetup) && is_array($themeSetup)) {
2171 foreach ($themeSetup as $key => $value) {
2172 if (!empty($value)) {
2173 $safeKey = preg_replace('/[^a-zA-Z0-9\-_]/', '', $key);
2174 $safeValue = preg_replace('/[;\{\}]/', '', $value);
2175 $colors .= "$safeKey: $safeValue;";
2176 }
2177 }
2178 }
2179
2180 return $colors;
2181 }
2182
2183 public function getPageLink($pageId = null): string
2184 {
2185 if (!$pageId || !get_post($pageId)) {
2186 return '';
2187 }
2188
2189 $link = get_page_link($pageId);
2190
2191 if (!empty($link)) {
2192 return Str::endsWith($link, '/') ? $link : $link . '/';
2193 }
2194 return '';
2195 }
2196
2197 }
2198