| @@ -7,8 +7,10 @@ | ||
| 7 | 7 | use FluentCart\App\CPT\Pages; |
| 8 | 8 | use FluentCart\App\Helpers\AddressHelper; |
| 9 | 9 | use FluentCart\App\Helpers\CurrenciesHelper; |
| 10 | 10 | use FluentCart\App\Services\OrderService; |
| 11 | +use FluentCart\App\Services\Theme\ColorPalette; | |
| 12 | +use FluentCart\App\Services\Theme\ThemePalette; | |
| 11 | 13 | use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; |
| 12 | 14 | use FluentCart\App\Modules\StoreManagedRenewal\Services\RenewalService; |
| 13 | 15 | use FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode; |
| 14 | 16 | use FluentCart\Framework\Support\Arr; |
| @@ -17,8 +19,11 @@ | ||
| 17 | 19 | use FluentCart\App\Services\Permission\PermissionManager; |
| 18 | 20 | |
| 19 | 21 | class StoreSettings implements ArrayableInterface |
| 20 | 22 | { |
| 23 | + const CACHE_KEY = 'store_settings'; | |
| 24 | + const CACHE_GROUP = 'fluentcart'; | |
| 25 | + | |
| 21 | 26 | /** |
| 22 | 27 | * @var string |
| 23 | 28 | * |
| 24 | 29 | * Store settings option name |
| @@ -33,8 +38,14 @@ | ||
| 33 | 38 | protected array $storeSettings; |
| 34 | 39 | |
| 35 | 40 | protected static $cachedStoreSettings = null; |
| 36 | 41 | |
| 42 | + public static function clearCache(): void | |
| 43 | + { | |
| 44 | + self::$cachedStoreSettings = null; | |
| 45 | + wp_cache_delete(self::CACHE_KEY, self::CACHE_GROUP); | |
| 46 | + } | |
| 47 | + | |
| 37 | 48 | public function __construct() |
| 38 | 49 | { |
| 39 | 50 | if (self::$cachedStoreSettings !== null) { |
| 40 | 51 | $this->storeSettings = self::$cachedStoreSettings; |
| @@ -71,8 +82,10 @@ | ||
| 71 | 82 | 'show_cart_icon_in_body' => 'yes', |
| 72 | 83 | 'additional_address_field' => 'yes', |
| 73 | 84 | 'hide_coupon_field' => 'no', |
| 74 | 85 | 'user_account_creation_mode' => 'all', |
| 86 | + 'auto_login_after_account_creation' => 'yes', | |
| 87 | + 'require_customer_email_verification' => 'no', | |
| 75 | 88 | 'checkout_page_id' => '', |
| 76 | 89 | 'custom_payment_page_id' => '', |
| 77 | 90 | 'registration_page_id' => '', |
| 78 | 91 | 'login_page_id' => '', |
| @@ -77,8 +90,9 @@ | ||
| 77 | 90 | 'registration_page_id' => '', |
| 78 | 91 | 'login_page_id' => '', |
| 79 | 92 | 'cart_page_id' => '', |
| 80 | 93 | 'receipt_page_id' => '', |
| 94 | + 'order_review_page_id' => '', | |
| 81 | 95 | 'shop_page_id' => '', |
| 82 | 96 | 'customer_profile_page_id' => '', |
| 83 | 97 | 'customer_profile_page_slug' => '', |
| 84 | 98 | 'currency' => 'USD', |
| @@ -89,9 +103,13 @@ | ||
| 89 | 103 | 'store_postcode' => '', |
| 90 | 104 | 'store_state' => '', |
| 91 | 105 | 'show_relevant_product_in_single_page' => 'yes', |
| 92 | 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', | |
| 93 | 110 | 'order_mode' => 'test', |
| 111 | + 'subscription_mode_guard' => 'yes', | |
| 94 | 112 | 'variation_view' => 'both', |
| 95 | 113 | 'variation_columns' => 'masonry', |
| 96 | 114 | 'enable_early_payment_for_installment' => 'yes', |
| 97 | 115 | 'subscription_management_mode' => 'gateway_managed', |
| @@ -99,9 +117,19 @@ | ||
| 99 | 117 | 'modules_settings' => [], |
| 100 | 118 | 'min_receipt_number' => '1', |
| 101 | 119 | 'inv_prefix' => 'INV-', |
| 102 | 120 | 'weight_unit' => 'kg', |
| 103 | - 'dimension_unit' => 'cm' | |
| 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' | |
| 104 | 132 | ]; |
| 105 | 133 | |
| 106 | 134 | return apply_filters('fluent_cart/store_settings/values', $defaultSettings, []); |
| 107 | 135 | } |
| @@ -120,9 +148,10 @@ | ||
| 120 | 148 | 'shop_page_id' => $this->getShopPage(), |
| 121 | 149 | 'customer_profile_page_id' => $this->getCustomerProfilePage(), |
| 122 | 150 | 'cart_page_id' => $this->getCartPage(), |
| 123 | 151 | 'checkout_page_id' => $this->getCheckoutPage(), |
| 124 | - 'receipt_page_id' => $this->getReceiptPage() | |
| 152 | + 'receipt_page_id' => $this->getReceiptPage(), | |
| 153 | + 'order_review_page_id' => $this->getOrderReviewPage() | |
| 125 | 154 | ]; |
| 126 | 155 | $isProActive = App::isProActive(); |
| 127 | 156 | $proFeatureIcon = Vite::getAssetUrl('images/crown.svg'); |
| 128 | 157 | |
| @@ -288,9 +317,9 @@ | ||
| 288 | 317 | ], |
| 289 | 318 | ] |
| 290 | 319 | ], |
| 291 | 320 | |
| 292 | - 'settings_hr' => [ | |
| 321 | + 'date_time_hr' => [ | |
| 293 | 322 | 'type' => 'html', |
| 294 | 323 | 'value' => '<hr class="settings-divider">' |
| 295 | 324 | ], |
| 296 | 325 | |
| @@ -556,8 +585,79 @@ | ||
| 556 | 585 | ], |
| 557 | 586 | ] |
| 558 | 587 | ], |
| 559 | 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 → 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 → 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 | + | |
| 560 | 660 | // 'settings_hr_modal' => [ |
| 561 | 661 | // 'type' => 'html', |
| 562 | 662 | // 'value' => '<hr class="settings-divider">' |
| 563 | 663 | // ], |
| @@ -798,8 +898,45 @@ | ||
| 798 | 898 | ], |
| 799 | 899 | ] |
| 800 | 900 | ], |
| 801 | 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 | + | |
| 802 | 939 | 'hr4' => [ |
| 803 | 940 | 'type' => 'html', |
| 804 | 941 | 'value' => '<hr class="settings-divider">' |
| 805 | 942 | ], |
| @@ -857,9 +994,9 @@ | ||
| 857 | 994 | 'schema' => [ |
| 858 | 995 | 'label' => [ |
| 859 | 996 | 'type' => 'html', |
| 860 | 997 | 'value' => '<span class="setting-label">' . __('Single Product Setup', 'fluent-cart') . '</span> |
| 861 | - <div class="form-note">' . __("Control the display of relevant information.", 'fluent-cart') . '</div>' | |
| 998 | + <div class="form-note">' . __("Choose which sections to display on the single product page.", 'fluent-cart') . '</div>' | |
| 862 | 999 | ], |
| 863 | 1000 | 'fields' => [ |
| 864 | 1001 | 'type' => 'grid', |
| 865 | 1002 | 'columns' => [ |
| @@ -877,8 +1014,17 @@ | ||
| 877 | 1014 | "label" => __('Show Relevant In Product Modal', 'fluent-cart'), |
| 878 | 1015 | "type" => "checkbox", |
| 879 | 1016 | "value" => "no" |
| 880 | 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 | + ], | |
| 881 | 1027 | ] |
| 882 | 1028 | ] |
| 883 | 1029 | |
| 884 | 1030 | ] |
| @@ -1040,10 +1186,159 @@ | ||
| 1040 | 1186 | ], |
| 1041 | 1187 | |
| 1042 | 1188 | ] |
| 1043 | 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 | + ], | |
| 1044 | 1235 | ], |
| 1045 | 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 | + ], | |
| 1046 | 1341 | 'cart_and_checkout' => [ |
| 1047 | 1342 | 'title' => __('Cart & checkout', 'fluent-cart'), |
| 1048 | 1343 | 'show_title' => false, |
| 1049 | 1344 | 'type' => 'section', |
| @@ -1307,10 +1602,53 @@ | ||
| 1307 | 1602 | ] |
| 1308 | 1603 | ] |
| 1309 | 1604 | ] |
| 1310 | 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 | + ], | |
| 1311 | 1648 | ] |
| 1312 | 1649 | ], |
| 1650 | + 'appearance' => $this->getAppearanceSchema(), | |
| 1313 | 1651 | ] |
| 1314 | 1652 | ], |
| 1315 | 1653 | ]; |
| 1316 | 1654 | |
| @@ -1317,8 +1655,14 @@ | ||
| 1317 | 1655 | |
| 1318 | 1656 | // Only show weight/dimension unit fields to users with shipping-sensitive permission |
| 1319 | 1657 | if (PermissionManager::hasPermission(['store/sensitive'])) { |
| 1320 | 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 | + | |
| 1321 | 1665 | $storeSchema['weight_unit_grid'] = [ |
| 1322 | 1666 | 'type' => 'grid', |
| 1323 | 1667 | 'columns' => ['default' => 1, 'md' => 3], |
| 1324 | 1668 | 'disable_nesting' => true, |
| @@ -1367,8 +1711,17 @@ | ||
| 1367 | 1711 | ] |
| 1368 | 1712 | ]; |
| 1369 | 1713 | } |
| 1370 | 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 | + | |
| 1371 | 1724 | return apply_filters("fluent_cart/store_settings/fields", $fields, []); |
| 1372 | 1725 | } |
| 1373 | 1726 | |
| 1374 | 1727 | public function isModuleTabEnabled(): bool |
| @@ -1425,8 +1778,9 @@ | ||
| 1425 | 1778 | |
| 1426 | 1779 | update_option($this->optionKey, $settings, true); |
| 1427 | 1780 | $this->storeSettings = $settings; |
| 1428 | 1781 | self::$cachedStoreSettings = $this->storeSettings; |
| 1782 | + wp_cache_delete(self::CACHE_KEY, self::CACHE_GROUP); | |
| 1429 | 1783 | |
| 1430 | 1784 | $isSlugChanged = Arr::get($prevSettings, 'product_slug') !== Arr::get($settings, 'product_slug'); |
| 1431 | 1785 | $isAccountPageChanged = Arr::get($prevSettings, 'customer_profile_page_slug') !== Arr::get($settings, 'customer_profile_page_slug'); |
| 1432 | 1786 | |
| @@ -1458,8 +1812,11 @@ | ||
| 1458 | 1812 | |
| 1459 | 1813 | public function isCheckoutPage(): bool |
| 1460 | 1814 | { |
| 1461 | 1815 | global $post; |
| 1816 | + if (!$post instanceof \WP_Post) { | |
| 1817 | + return false; | |
| 1818 | + } | |
| 1462 | 1819 | $pageId = $this->getCheckoutPageId(); |
| 1463 | 1820 | return intval($pageId) === intval($post->ID); |
| 1464 | 1821 | } |
| 1465 | 1822 | |
| @@ -1478,8 +1835,9 @@ | ||
| 1478 | 1835 | 'registration_page_id', |
| 1479 | 1836 | 'login_page_id', |
| 1480 | 1837 | 'cart_page_id', |
| 1481 | 1838 | 'receipt_page_id', |
| 1839 | + 'order_review_page_id', | |
| 1482 | 1840 | 'shop_page_id', |
| 1483 | 1841 | 'customer_profile_page_id', |
| 1484 | 1842 | 'customer_profile_page_slug', |
| 1485 | 1843 | ] |
| @@ -1555,9 +1913,29 @@ | ||
| 1555 | 1913 | } |
| 1556 | 1914 | return home_url(); |
| 1557 | 1915 | } |
| 1558 | 1916 | |
| 1917 | + public function getOrderReviewPageId() | |
| 1918 | + { | |
| 1919 | + return Arr::get($this->storeSettings, 'order_review_page_id'); | |
| 1920 | + } | |
| 1559 | 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 | + | |
| 1560 | 1938 | public function getBuyButtonText(): string |
| 1561 | 1939 | { |
| 1562 | 1940 | return esc_html(Arr::get($this->storeSettings, 'checkout_button_text', __('Buy now', 'fluent-cart'))); |
| 1563 | 1941 | } |
| @@ -1700,8 +2078,284 @@ | ||
| 1700 | 2078 | return $slug; |
| 1701 | 2079 | } |
| 1702 | 2080 | |
| 1703 | 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); | |
| 1704 | 2358 | } |
| 1705 | 2359 | |
| 1706 | 2360 | /** |
| 1707 | 2361 | * Get theme colors as CSS variable string. |