PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / assets.php

assets.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/assets.php

744 lines 27.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine;
4
5 use StoreEngine\Admin\Notices;
6 use StoreEngine\Classes\Countries;
7 use StoreEngine\Classes\Order;
8 use StoreEngine\Classes\Product\VariableProduct;
9 use StoreEngine\Utils\ArrayUtil;
10 use StoreEngine\Utils\Formatting;
11 use StoreEngine\Utils\Helper;
12 use StoreEngine\Utils\PaymentUtil;
13 use StoreEngine\Utils\Pixel;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit(); // Exit if accessed directly.
17 }
18
19 class Assets {
20
21 public static function init() {
22 $self = new self();
23 add_action( 'admin_enqueue_scripts', array( $self, 'enqueue_admin_menu_css' ) );
24 add_action( 'admin_enqueue_scripts', [ $self, 'backend_scripts' ] );
25 add_action( 'admin_enqueue_scripts', [ $self, 'backend_inline_style' ] );
26 add_action( 'wp_enqueue_scripts', [ $self, 'frontend_scripts' ] );
27 add_action( 'enqueue_block_editor_assets', [ $self, 'block_editor_assets' ] );
28 add_action( 'wp_footer', [ $self, 'display_price_placeholder' ] );
29 }
30
31 public function web_fonts_url( $font ): string {
32 $font_url = add_query_arg( 'family', rawurlencode( $font ), '//fonts.googleapis.com/css2' );
33
34 return add_query_arg( 'display', 'swap', $font_url );
35 }
36
37 public function enqueue_admin_menu_css( $hook ) {
38 // Single combined admin stylesheet (menu chrome + notices) on every admin
39 // screen — replaces the old menu.css / notices.css / icon-font trio.
40 // Notice icons are inline SVG (see Notices::get_svg_icon), so the icon
41 // font is no longer needed on this always-loaded path.
42 wp_enqueue_style( 'storeengine-admin', STOREENGINE_ASSETS_URI . 'css/admin.css', [], STOREENGINE_VERSION, 'all' );
43
44 if ( strpos( $hook, '_page_' . STOREENGINE_PLUGIN_SLUG ) !== false ) {
45 return;
46 }
47
48 Notices::init()->dispatch_notices();
49 if ( ! empty( Admin\Notices::get_notices() ) ) {
50 wp_add_inline_style( 'storeengine-admin', $this->get_dynamic_css() );
51 }
52 }
53
54 public function frontend_scripts() {
55 // Icon Library.
56 wp_enqueue_style(
57 'storeengine-frontend-icon',
58 STOREENGINE_ASSETS_URI . 'library/icons/storeengine-icons.css',
59 [ 'wp-components' ],
60 filemtime(
61 STOREENGINE_ASSETS_DIR_PATH .
62 'library/icons/storeengine-icons.css'
63 ),
64 'all'
65 );
66
67 // Main Stylesheet.
68 wp_enqueue_style(
69 'storeengine-frontend-style',
70 STOREENGINE_ASSETS_URI . 'build/frontend.css',
71 [],
72 filemtime( STOREENGINE_ASSETS_DIR_PATH . 'build/frontend.css' ),
73 'all'
74 );
75
76 // Add dynamic CSS variables.
77 wp_add_inline_style( 'storeengine-frontend-style', $this->get_dynamic_css() );
78
79 // Add Flickity CSS.
80 if ( Helper::is_product() ) {
81 wp_enqueue_style(
82 'flickity',
83 STOREENGINE_ASSETS_URI . 'library/flickity/flickity.min.css',
84 [],
85 STOREENGINE_VERSION,
86 null
87 );
88 }
89
90 // JS — use `include` (not `include_once`) so a second hook invocation in
91 // the same request still gets the array; `*_once` returns int(1) after
92 // the first include and breaks the array_merge below.
93 $asset_file = STOREENGINE_ASSETS_DIR_PATH . sprintf( 'build/frontend.%s.asset.php', STOREENGINE_VERSION );
94 $dependencies = is_file( $asset_file ) ? include $asset_file : null;
95 if ( ! is_array( $dependencies ) ) {
96 $dependencies = [ 'dependencies' => [], 'version' => STOREENGINE_VERSION ];
97 }
98
99 do_action( 'storeengine/enqueue_frontend_scripts' );
100
101 // Note: the storefront bundle is small enough that the standard string
102 // extractor parses it fine, so w.org generates its language pack normally
103 // and it needs no i18n companion — we deliberately don't add that
104 // multi-KB manifest to every storefront page.
105 wp_enqueue_script(
106 'storeengine-frontend-scripts',
107 STOREENGINE_ASSETS_URI .
108 sprintf( 'build/frontend.%s.js', STOREENGINE_VERSION ),
109 // The storefront bundle is jQuery-free (vanilla JS via StoreEngineDQ);
110 // `wp-util` was unused and only pulled jQuery in transitively.
111 (array) ( $dependencies['dependencies'] ?? [] ),
112 $dependencies['version'] ?? STOREENGINE_VERSION,
113 true
114 );
115
116 if ( Helper::is_product() ) {
117 global $product;
118
119 if ( $product instanceof VariableProduct && 'variable' === $product->get_type() ) {
120 wp_localize_script(
121 'storeengine-frontend-scripts',
122 'StoreEngineProductVariations',
123 self::get_product_variations( $product )
124 );
125 }
126
127 }
128
129 $analytics = (array) Helper::get_settings( 'analytics', [] );
130 if ( ArrayUtil::any( $analytics, fn( $stat ) => true === $stat ) ) {
131 $data = [];
132 if ( Helper::is_product() ) {
133 $data = Pixel::get_single_product_data();
134 }
135 if ( Helper::is_shop() ) {
136 $data = Pixel::get_product_archive_data();
137 }
138 if ( Helper::is_cart() || Helper::is_checkout() ) {
139 $data = [ 'cart_info' => Pixel::get_cart_info() ];
140 }
141 if ( Helper::is_thank_you() ) {
142 // Provide the completed order so the GA4 `purchase` event can
143 // fire on the thank-you page (see google.ts), attributing the
144 // conversion + revenue to this URL instead of the checkout page.
145 $data = $this->get_thankyou_pixel_data();
146 }
147 wp_localize_script(
148 'storeengine-frontend-scripts',
149 'SEPixelData',
150 array_merge(
151 [
152 'page_title' => wp_get_document_title(),
153 'currency' => Formatting::get_currency(),
154 ],
155 $data
156 )
157 );
158 }
159
160 wp_localize_script(
161 'storeengine-frontend-scripts',
162 'StoreEngineGlobal',
163 $this->get_frontend_script_data()
164 );
165
166 if ( Helper::get_settings( 'enable_wishlist', false ) ) {
167 wp_localize_script(
168 'storeengine-frontend-scripts',
169 'StoreEngineWishlist',
170 [
171 'enabled' => true,
172 'isLoggedIn' => is_user_logged_in(),
173 'ids' => is_user_logged_in() ? array_map( 'strval', storeengine_get_user_wishlist() ) : [],
174 'restBase' => esc_url_raw( rest_url( 'storeengine/v1/wishlist/' ) ),
175 'nonce' => wp_create_nonce( 'wp_rest' ),
176 'i18n' => [
177 'add' => __( 'Add to wishlist', 'storeengine' ),
178 'remove' => __( 'Remove from wishlist', 'storeengine' ),
179 ],
180 ]
181 );
182 }
183
184 if ( Helper::get_settings( 'enable_product_compare', false ) ) {
185 wp_localize_script(
186 'storeengine-frontend-scripts',
187 'StoreEngineCompare',
188 [
189 'enabled' => true,
190 'isLoggedIn' => is_user_logged_in(),
191 'ids' => is_user_logged_in() ? array_map( 'strval', storeengine_get_user_compare() ) : [],
192 'max' => \StoreEngine\Classes\ProductCompare::max(),
193 'restBase' => esc_url_raw( rest_url( 'storeengine/v1/compare/' ) ),
194 'nonce' => wp_create_nonce( 'wp_rest' ),
195 'i18n' => [
196 'add' => __( 'Compare', 'storeengine' ),
197 'added' => __( 'Comparing', 'storeengine' ),
198 'remove' => __( 'Remove from comparison', 'storeengine' ),
199 'title' => __( 'Compare products', 'storeengine' ),
200 'close' => __( 'Close', 'storeengine' ),
201 ],
202 ]
203 );
204 }
205
206 wp_set_script_translations(
207 'storeengine-frontend-scripts',
208 'storeengine',
209 STOREENGINE_ROOT_DIR_PATH . 'i18n/languages'
210 );
211
212 // Add Flickity JS
213 if ( Helper::is_product() ) {
214 wp_enqueue_script(
215 'flickity',
216 STOREENGINE_ASSETS_URI . 'library/flickity/flickity.pkgd.min.js',
217 [],
218 STOREENGINE_VERSION,
219 true
220 );
221 }
222
223 do_action( 'storeengine/assets/after_frontend_scripts' );
224 }
225
226 /**
227 * Completed-order data for the thank-you page GA4 `purchase` event.
228 *
229 * Returned under an `order` key inside SEPixelData and consumed by
230 * dev_storeengine/pixel-integrations/google.ts. The shape mirrors the
231 * checkout REST response's `order`, limited to the fields the analytics
232 * integration reads. Building it here (rather than re-running the checkout
233 * response pipeline) avoids re-firing payment-success side effects.
234 *
235 * @return array Either [ 'order' => [...] ] or [] when no valid order.
236 */
237 private function get_thankyou_pixel_data(): array {
238 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
239 $order_hash = isset( $_GET['order_hash'] ) ? sanitize_text_field( wp_unslash( $_GET['order_hash'] ) ) : '';
240 if ( '' === $order_hash ) {
241 return [];
242 }
243
244 try {
245 $order = Helper::get_order_by_key( $order_hash );
246 } catch ( \Throwable $e ) {
247 return [];
248 }
249
250 if ( ! $order instanceof Order || is_wp_error( $order ) || ! $order->get_id() ) {
251 return [];
252 }
253
254 $line_items = [];
255 foreach ( $order->get_items( 'line_item' ) as $item ) {
256 $meta = [];
257 foreach ( (array) $item->get_meta_data() as $m ) {
258 if ( is_object( $m ) && isset( $m->key ) ) {
259 $meta[] = [
260 'key' => $m->key,
261 'value' => $m->value ?? '',
262 ];
263 }
264 }
265
266 $line_items[] = [
267 'product_id' => $item->get_product_id(),
268 'price_id' => $item->get_price_id(),
269 'name' => $item->get_name(),
270 'price' => $item->get_price(),
271 'quantity' => $item->get_quantity(),
272 'subtotal' => $item->get_subtotal(),
273 'total' => $item->get_total(),
274 'variation_id' => $item->get_variation_id(),
275 'price_type' => $item->get_price_type(),
276 'meta' => $meta,
277 ];
278 }
279
280 $coupon_lines = array_map(
281 static function ( $code ) {
282 return [ 'code' => $code ];
283 },
284 array_values( array_filter( (array) $order->get_coupon_codes() ) )
285 );
286
287 return [
288 'order' => [
289 'transaction_id' => $order->get_transaction_id(),
290 'order_key' => $order->get_order_key(),
291 'currency' => $order->get_currency(),
292 'total' => $order->get_total(),
293 'total_tax' => $order->get_total_tax(),
294 'shipping_total_amount' => $order->get_shipping_total(),
295 'discount_total_amount' => $order->get_discount_total(),
296 'coupon_lines' => $coupon_lines,
297 'line_items' => $line_items,
298 ],
299 ];
300 }
301
302 /**
303 * Enqueue Block Editor Assets
304 *
305 * @since 1.3.3
306 */
307 public function block_editor_assets() {
308 // Check if the aBlocks plugin is active before enqueuing styles.
309 if ( ! Helper::is_plugin_active( 'ablocks/ablocks.php' ) ) {
310 return;
311 }
312
313 // Icon Library.
314 wp_enqueue_style(
315 'storeengine-frontend-icon',
316 STOREENGINE_ASSETS_URI . 'library/icons/storeengine-icons.css',
317 [ 'wp-components' ],
318 filemtime(
319 STOREENGINE_ASSETS_DIR_PATH .
320 'library/icons/storeengine-icons.css'
321 ),
322 'all'
323 );
324
325 // Main Stylesheet.
326 wp_enqueue_style(
327 'storeengine-frontend-style',
328 STOREENGINE_ASSETS_URI . 'build/frontend.css',
329 [],
330 filemtime( STOREENGINE_ASSETS_DIR_PATH . 'build/frontend.css' ),
331 'all'
332 );
333
334 // Add dynamic CSS variables.
335 wp_add_inline_style( 'storeengine-frontend-style', $this->get_dynamic_css() );
336
337 // Add Flickity CSS.
338 wp_enqueue_style(
339 'flickity',
340 STOREENGINE_ASSETS_URI . 'library/flickity/flickity.min.css',
341 [],
342 STOREENGINE_VERSION,
343 null
344 );
345 }
346
347 public static function get_product_variations( VariableProduct $product ): array {
348 $pricing = [];
349 $taxonomies = [];
350 $variations = array_map( function ( $variant ) use ( &$taxonomies ) {
351 $attributes = [];
352 foreach ( $variant->get_attributes() as $attribute ) {
353 $attributes[ $attribute->taxonomy ] = $attribute->slug;
354 $taxonomies[] = $attribute->taxonomy;
355 }
356
357 return [
358 'id' => $variant->get_id(),
359 'name' => $variant->get_name(),
360 'price' => $variant->get_price(),
361 'sku' => $variant->get_sku(),
362 'pricing_id' => (int) $variant->get_pricing_id(),
363 'featured_image_url' => wp_get_attachment_image_url( $variant->get_featured_image() ),
364 'attributes' => $attributes,
365 // Per-variation stock so the picker can disable Add to Cart /
366 // relabel "Out of stock" for the selected variation.
367 'is_in_stock' => method_exists( $variant, 'is_in_stock' ) ? $variant->is_in_stock() : true,
368 'stock_status' => method_exists( $variant, 'get_stock_status' ) ? $variant->get_stock_status() : 'instock',
369 ];
370 }, $product->get_available_variants() );
371
372 foreach ( $product->get_prices() as $price ) {
373 $pricing[ $price->get_id() ] = $price->get_price();
374 }
375
376 return [
377 'taxonomies' => array_values( array_unique( $taxonomies ) ),
378 'pricing' => $pricing,
379 'variations' => $variations,
380 ];
381 }
382
383 public function get_frontend_script_data() {
384 $data = [];
385
386 if ( Helper::is_checkout() || Helper::is_edit_address_page() ) {
387 $state_labels = array_filter( array_map( fn( $data ) => [
388 'label' => $data['state']['label'] ?? '',
389 'required' => $data['state']['required'] ?? false,
390 ], Countries::init()->get_country_locale() ) );
391 $states = array_merge( Countries::init()->get_allowed_country_states(), Countries::init()->get_shipping_country_states() );
392 $data = [
393 'states' => $states,
394 'state_labels' => $state_labels,
395 'state_label' => __( 'State / County', 'storeengine' ),
396 'is_required' => '&nbsp;<abbr class="storeengine-required" title="' . esc_attr__( 'Required', 'storeengine' ) . '">*</abbr>',
397 'i18n_select_state_text' => esc_attr__( 'Select an option&hellip;', 'storeengine' ),
398 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'storeengine' ),
399 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'storeengine' ),
400 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'storeengine' ),
401 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'storeengine' ),
402 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'storeengine' ),
403 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'storeengine' ),
404 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'storeengine' ),
405 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'storeengine' ),
406 'i18n_load_more' => _x( 'Loading more results&hellip;', 'enhanced select', 'storeengine' ),
407 'i18n_searching' => _x( 'Searching&hellip;', 'enhanced select', 'storeengine' ),
408 ];
409 }
410
411 $analytics = (array) Helper::get_settings( 'analytics', [] );
412 $hasAnalytics = ArrayUtil::any( $analytics, fn( $stat ) => true === $stat );
413
414 return apply_filters(
415 'storeengine/frontend_scripts_data',
416 array_merge(
417 $this->_get_script_data(),
418 [
419 'checkout_page_url' => esc_url( Helper::get_checkout_url() ),
420 'thankyou_page_url' => esc_url( Helper::get_thankyou_page_url() ),
421 'payment_gateways' => $this->get_payment_method_script_data(),
422 'payment_data' => [],
423 'checkout_with_order_pay' => Formatting::string_to_bool( get_query_var( 'order_pay' ) ),
424 'page' => [
425 'dashboard' => esc_url( Helper::get_dashboard_url() ),
426 'checkout' => esc_url( Helper::get_checkout_url() ),
427 'thankyou' => esc_url( Helper::get_thankyou_page_url() ),
428 'terms' => esc_url( Helper::get_terms_page_url() ),
429 'privacy' => esc_url( Helper::get_privacy_page_url() ),
430 ],
431 'is_page' => [
432 'is_product' => Helper::is_product(),
433 'is_shop' => Helper::is_shop(),
434 'is_cart' => Helper::is_cart(),
435 'is_dashboard' => Helper::is_dashboard_index(),
436 'is_address_edit' => Helper::is_edit_address_page(),
437 'is_checkout' => Helper::is_checkout(),
438 'is_order_pay' => Helper::is_checkout() && PaymentUtil::is_valid_order_pay_page(),
439 'is_add_payment_method' => Helper::is_add_payment_method_page(),
440 ],
441 'settings' => [
442 'analytics' => $hasAnalytics ? $analytics : false,
443 'auto_open_cart_drawer' => (bool) Helper::get_settings( 'auto_open_cart_drawer', false ),
444 ]
445 ],
446 $data
447 )
448 );
449 }
450
451 public function _get_script_data(): array {
452 global $storeengine_addons;
453
454 $current_user = wp_get_current_user();
455 $user_name = ! is_email( $current_user->display_name ) ? $current_user->display_name : trim( $current_user->first_name . ' ' . $current_user->last_name );
456 $user_name = $user_name ?: $current_user->user_login;
457
458 // Brand logo for barcode labels etc. — StoreEngine store logo, then the
459 // theme custom logo, then the site icon.
460 $store_logo = Helper::get_settings( 'store_logo' );
461 if ( is_numeric( $store_logo ) && (int) $store_logo > 0 ) {
462 $store_logo = wp_get_attachment_image_url( (int) $store_logo, 'medium' );
463 }
464 if ( empty( $store_logo ) && get_theme_mod( 'custom_logo' ) ) {
465 $store_logo = wp_get_attachment_image_url( (int) get_theme_mod( 'custom_logo' ), 'medium' );
466 }
467 if ( empty( $store_logo ) ) {
468 $store_logo = get_site_icon_url( 128 );
469 }
470
471 $data = [
472 'is_admin' => is_admin(),
473 'nonce' => wp_create_nonce( 'wp_rest' ),
474 'storeengine_nonce' => wp_create_nonce( 'storeengine_nonce' ),
475 'rest_url' => esc_url_raw( rest_url() ),
476 'locale' => get_locale(),
477 'user_locale' => function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale(),
478 'namespace' => STOREENGINE_PLUGIN_SLUG . '/v1/',
479 'plugin_root_url' => STOREENGINE_PLUGIN_ROOT_URI,
480 'plugin_root_path' => STOREENGINE_ROOT_DIR_PATH,
481 'root_path' => STOREENGINE_ROOT_DIR_PATH,
482 'root_url' => STOREENGINE_PLUGIN_ROOT_URI,
483 'assets_url' => STOREENGINE_ASSETS_URI,
484 'ajaxurl' => esc_url( admin_url( 'admin-ajax.php' ) ),
485 'admin_url' => admin_url(),
486 'site_url' => site_url(),
487 'route_path' => wp_parse_url( admin_url(), PHP_URL_PATH ),
488 'admin_menu_lists' => wp_json_encode( Admin\Menu::get_menu_tree() ),
489 'current_user_id' => $current_user->ID,
490 'current_user_name' => $user_name,
491 'store_email' => Helper::get_settings( 'store_email' ),
492 'store_name' => get_bloginfo( 'name' ),
493 'store_logo' => $store_logo ? $store_logo : '',
494 'is_rtl' => is_rtl(),
495 'addons' => $storeengine_addons,
496 // Addon cards injected by other plugins (e.g. storeengine-payments
497 // gateways) for the Add-ons screen. Each entry mirrors the card shape
498 // in the React Addons utils (name, label, details, tags, svgIcon, …).
499 'extra_addons' => apply_filters( 'storeengine/admin/extra_addons', [] ),
500 // Install-only teasers for the free companion plugins (StoreEngine
501 // Payments / Connectors). Each entry carries the plugin's active state,
502 // a download URL, and a catalog of what it unlocks — the React admin
503 // shows the teaser only while the companion plugin is inactive.
504 'satellite_plugins' => \StoreEngine\Admin\SatellitePlugins::get_teaser_data(),
505 'timezone' => wp_timezone_string(),
506 'current_user_can' => [
507 'manage_options' => current_user_can( 'manage_options' ),
508 ],
509 'screen_options' => \StoreEngine\Admin\ScreenOptions::get_all(),
510 'currency_options' => Helper::get_currency_options(),
511 'localeConv' => localeconv(),
512 'is_tax_enabled' => Helper::get_settings( 'enable_product_tax' ),
513 'enable_product_tax' => Helper::get_settings( 'enable_product_tax' ),
514 'is_pro' => Helper::is_active_storeengine_pro(),
515 'ablocks_status' => Helper::is_plugin_installed( 'ablocks/ablocks.php' ) ? ( Helper::is_plugin_active( 'ablocks/ablocks.php' ) ? 'active' : 'not-active' ) : 'not-installed',
516 ];
517
518 /**
519 * Filters the admin app's localized data (window.StoreEngineGlobal).
520 *
521 * Addons can inject extra flags here. The Role & Permission addon uses it
522 * to add the current user's per-resource write permissions so the CPT
523 * editors can render read-only (hide Save) for a view-only staff user.
524 *
525 * @param array $data Localized data array.
526 */
527 return apply_filters( 'storeengine/admin/script_data', $data );
528 }
529
530 public function get_payment_method_script_data() {
531 $data = apply_filters( 'storeengine/frontend_scripts_payment_method_data', [] );
532
533 // Also walk every available gateway through the unified per-gateway
534 // data filter — `storeengine/checkout/gateway/{id}/data` — so third-
535 // party gateways that only hook the new public API still populate the
536 // legacy `payment_gateways.{id}` global that the /checkout/ page
537 // shells read from. The same filter feeds React Quick Checkout's
538 // snapshot, so one hook covers both surfaces.
539 try {
540 $gateways = Helper::get_payment_gateways()->get_available_payment_gateways();
541 } catch ( \Throwable $e ) {
542 $gateways = [];
543 }
544 foreach ( $gateways as $gateway ) {
545 $existing = isset( $data[ $gateway->id ] ) && is_array( $data[ $gateway->id ] ) ? $data[ $gateway->id ] : [];
546 $merged = apply_filters( "storeengine/checkout/gateway/{$gateway->id}/data", $existing, $gateway );
547 if ( is_array( $merged ) && ! empty( $merged ) ) {
548 $data[ $gateway->id ] = $merged;
549 }
550 }
551
552 return $data;
553 }
554
555 /**
556 * Register the JS-translation companion handle.
557 *
558 * The real admin/setup/frontend bundles are multi-megabyte webpack builds
559 * that the standard WP-CLI / translate.wordpress.org string extractor cannot
560 * parse, so on w.org no `storeengine-<locale>-<md5(bundle)>.json` language
561 * pack is ever generated and the screens stay untranslated. We ship one
562 * small, parseable file — assets/build/i18n-strings.js (generated from the
563 * POT by bin/make-i18n-strings.mjs) — that lists every JS string. w.org can
564 * read it and builds `storeengine-<locale>-<md5(i18n-strings.js)>.json`.
565 * Because @wordpress/i18n keys locale data by TEXT-DOMAIN (not by file),
566 * loading that one JSON translates every bundle. The path is unversioned so
567 * the md5 — and the language packs — stay stable across releases.
568 *
569 * Enqueue this handle as a dependency of any StoreEngine bundle; that both
570 * loads the JSON and makes @wordpress/i18n available.
571 */
572 public function register_i18n_strings() {
573 if ( wp_script_is( 'storeengine-i18n', 'registered' ) ) {
574 return;
575 }
576
577 $file = STOREENGINE_ASSETS_DIR_PATH . 'build/i18n-strings.js';
578 if ( ! is_file( $file ) ) {
579 return;
580 }
581
582 wp_register_script(
583 'storeengine-i18n',
584 STOREENGINE_ASSETS_URI . 'build/i18n-strings.js',
585 [ 'wp-i18n' ],
586 filemtime( $file ),
587 true
588 );
589 wp_set_script_translations(
590 'storeengine-i18n',
591 'storeengine',
592 STOREENGINE_ROOT_DIR_PATH . 'i18n/languages'
593 );
594 }
595
596 /**
597 * Enqueue Files on Start Plugin
598 *
599 * @param string $hook
600 *
601 * @function backend_scripts
602 */
603 public function backend_scripts( string $hook ) {
604 if ( ! strpos( $hook, '_page_' . STOREENGINE_PLUGIN_SLUG ) !== false ) {
605 return;
606 }
607
608 remove_all_actions( 'admin_notices' );
609
610 wp_enqueue_style(
611 'storeengine-admin-icon',
612 STOREENGINE_ASSETS_URI . 'library/icons/storeengine-icons.css',
613 [ 'wp-components' ],
614 filemtime(
615 STOREENGINE_ASSETS_DIR_PATH .
616 'library/icons/storeengine-icons.css'
617 ),
618 );
619
620 wp_enqueue_style(
621 'storeengine-admin-style',
622 STOREENGINE_ASSETS_URI . 'build/backend.css',
623 [ 'wp-components' ],
624 filemtime( STOREENGINE_ASSETS_DIR_PATH . 'build/backend.css' ),
625 'all'
626 );
627
628 wp_add_inline_style( 'storeengine-admin-style', $this->get_dynamic_css() );
629
630 if ( ! did_action( 'wp_enqueue_media' ) ) {
631 wp_enqueue_media();
632 }
633
634 // js — see note on the frontend equivalent above (use `include` so a
635 // second invocation in the same request still returns the array).
636 $asset_file = STOREENGINE_ASSETS_DIR_PATH . sprintf( 'build/backend.%s.asset.php', STOREENGINE_VERSION );
637 $dependencies = is_file( $asset_file ) ? include $asset_file : null;
638 if ( ! is_array( $dependencies ) ) {
639 $dependencies = [ 'dependencies' => [], 'version' => STOREENGINE_VERSION ];
640 }
641
642 // Load the JS-translation companion so w.org language packs apply here.
643 $this->register_i18n_strings();
644
645 wp_enqueue_script(
646 'storeengine-admin-scripts',
647 STOREENGINE_ASSETS_URI .
648 sprintf( 'build/backend.%s.js', STOREENGINE_VERSION ),
649 array_merge( (array) ( $dependencies['dependencies'] ?? [] ), wp_script_is( 'storeengine-i18n', 'registered' ) ? [ 'storeengine-i18n' ] : [] ),
650 $dependencies['version'] ?? STOREENGINE_VERSION,
651 true
652 );
653 wp_localize_script(
654 'storeengine-admin-scripts',
655 'StoreEngineGlobal',
656 $this->get_backend_script_data()
657 );
658 wp_set_script_translations(
659 'storeengine-admin-scripts',
660 'storeengine',
661 STOREENGINE_ROOT_DIR_PATH . 'i18n/languages'
662 );
663 }
664
665 public function get_backend_script_data(): array {
666 $currencies = [];
667
668 foreach ( Helper::get_currencies() as $code => $label ) {
669 $currencies[] = [
670 'code' => $code,
671 'name' => $label,
672 'symbol' => Helper::get_currency_symbol( $code ),
673 ];
674 }
675
676 return apply_filters(
677 'storeengine/backend_scripts_data',
678 array_merge(
679 $this->_get_script_data(),
680 [
681 'first_order' => Helper::get_first_order_date( 'Y-m-d' ),
682 'currency_options' => Helper::get_currency_options(),
683 'localeConv' => localeconv(),
684 'countries' => Countries::init()->get_countries(),
685 'currencies' => $currencies,
686 'enable_after_purchase_redirect' => Helper::get_settings( 'enable_after_purchase_redirect', false ),
687 'enable_faqs' => (bool) Helper::get_settings( 'enable_faqs', true ),
688 'faq_mode' => Helper::get_settings( 'faq_mode', 'global' ),
689 // Store-wide default for new products. Settings → Products
690 // exposes a radio that writes this; the product editor's
691 // initial-values builder reads it for new products only.
692 'default_product_shipping_type' => in_array( Helper::get_settings( 'default_product_shipping_type', 'digital' ), [ 'digital', 'physical' ], true ) ? Helper::get_settings( 'default_product_shipping_type', 'digital' ) : 'digital',
693 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
694 ]
695 )
696 );
697 }
698
699 public function get_dynamic_css(): string {
700 global $storeengine_settings;
701
702 /**
703 * Filter css properties
704 */
705 $css = apply_filters( 'storeengine/dynamic_css_root_properties', [
706 '--storeengine-primary-color' => esc_attr( $storeengine_settings->global_primary_color ?? '' ),
707 '--storeengine-secondary-color' => esc_attr( $storeengine_settings->global_secondary_color ?? '' ),
708 '--storeengine-text-color' => esc_attr( $storeengine_settings->global_text_color ?? '' ),
709 '--storeengine-subtitle-color' => esc_attr( $storeengine_settings->global_subtitle_color ?? '' ),
710 '--storeengine-input-text-color' => esc_attr( $storeengine_settings->global_input_text_color ?? '' ),
711 '--storeengine-placeholder-color' => esc_attr( $storeengine_settings->global_placeholder_color ?? '' ),
712 '--storeengine-border-color' => esc_attr( $storeengine_settings->global_border_color ?? '' ),
713 '--storeengine-background-color' => esc_attr( $storeengine_settings->global_background_color ?? '' ),
714 ] );
715
716 // Filter out empty values to avoid invalid CSS.
717 $filtered_css = array_filter( $css, fn( $value ) => trim( ltrim( rtrim( $value, ';' ), ':' ) ) !== '' );
718
719 return ':root {' . PHP_EOL . implode( PHP_EOL, array_map( fn( $key, $value ) => "\t$key:$value;", array_keys( $filtered_css ), $filtered_css ) ) . PHP_EOL . '}';
720 }
721
722 public function display_price_placeholder() {
723 ?>
724 <script type="text/html" id="tmpl-storeengine-price">
725 <p class="storeengine-product__price-amount">
726 {{{ data.price }}}
727 </p>
728 </script>
729 <?php
730 }
731
732 public function backend_inline_style() {
733 $custom_css = '
734 a[href*="page=storeengine-get-pro"] {
735 background: #FFA500 !important;
736 color: #000000 !important;
737 font-weight: 600;
738 box-shadow: none !important;
739 }
740 ';
741 wp_add_inline_style( 'admin-bar', $custom_css );
742 }
743 }
744