PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
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 1.3.20 All 50 releases
fluent-cart / api / StoreSettings.php

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

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