PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
fluent-cart / api / StoreSettings.php

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

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