PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.2
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.2
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Admin / Menu.php

Menu.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.2, at includes/Admin/Menu.php

672 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Admin Menu Class.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see http://wcpos.com
8 * @package WCPOS\WooCommercePOS
9 */
10
11 namespace WCPOS\WooCommercePOS\Admin;
12
13 use WCPOS\WooCommercePOS\Services\Analytics;
14 use WCPOS\WooCommercePOS\Services\Analytics_Profile;
15 use WCPOS\WooCommercePOS\Services\Landing_Profile;
16 use WCPOS\WooCommercePOS\Services\Lifecycle_Events;
17 use WCPOS\WooCommercePOS\Services\Settings;
18 use const WCPOS\WooCommercePOS\PLUGIN_NAME;
19 use const WCPOS\WooCommercePOS\PLUGIN_URL;
20 use const WCPOS\WooCommercePOS\TRANSLATION_VERSION;
21 use const WCPOS\WooCommercePOS\VERSION as PLUGIN_VERSION;
22
23 /**
24 * Menu class.
25 */
26 class Menu {
27 /**
28 * Unique top level menu identifier.
29 *
30 * @var string
31 */
32 public $toplevel_screen_id;
33
34 /**
35 * Unique top level menu identifier.
36 *
37 * @var string
38 */
39 public $settings_screen_id;
40
41 /**
42 * Gallery submenu page hook suffix.
43 *
44 * @var string
45 */
46 public $gallery_screen_id;
47
48 /**
49 * View POS submenu page hook suffix.
50 *
51 * @var string
52 */
53 public $view_pos_screen_id;
54
55 /**
56 * Constructor.
57 */
58 public function __construct() {
59 if ( current_user_can( 'manage_woocommerce_pos' ) ) {
60 $this->register_pos_admin();
61 add_filter( 'custom_menu_order', '__return_true' );
62 add_filter( 'menu_order', array( $this, 'menu_order' ), 9, 1 );
63 add_filter( 'parent_file', array( $this, 'highlight_templates_menu' ) );
64 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_landing_scripts_and_styles' ) );
65 add_action( 'admin_footer', array( $this, 'print_upgrade_click_tracking_script' ) );
66 add_action( 'admin_init', array( $this, 'redirect_template_list_page' ) );
67 }
68
69 // add_filter( 'woocommerce_analytics_report_menu_items', array( $this, 'analytics_menu_items' ) );.
70 }
71
72 /**
73 * Filters the order of administration menu items.
74 *
75 * A truthy value must first be passed to the {@see 'custom_menu_order'} filter
76 * for this filter to work. Use the following to enable custom menu ordering:
77 *
78 * add_filter( 'custom_menu_order', '__return_true' );
79 *
80 * @param array $menu_order An ordered array of menu items.
81 *
82 * @return array
83 */
84 public function menu_order( array $menu_order ): array {
85 $woo = array_search( 'woocommerce', $menu_order, true );
86 $pos = array_search( PLUGIN_NAME, $menu_order, true );
87
88 if ( false !== $woo && false !== $pos ) {
89 // rearrange menu.
90 unset( $menu_order[ $pos ] );
91 array_splice( $menu_order, ++$woo, 0, PLUGIN_NAME );
92
93 // rearrange submenu.
94 global $submenu;
95 $pos_submenu = &$submenu[ PLUGIN_NAME ];
96 $pos_submenu[500] = $pos_submenu[1];
97 unset( $pos_submenu[1] );
98 }
99
100 return $menu_order;
101 }
102
103 /**
104 * Render the upgrade page.
105 */
106 public function display_upgrade_page(): void {
107 include_once 'views/upgrade.php';
108 }
109
110 /**
111 * Add POS submenu to WooCommerce Analytics menu.
112 *
113 * @param array $report_pages The analytics report pages.
114 */
115 public function analytics_menu_items( array $report_pages ): array {
116 // Find the position of the 'Orders' item.
117 $position = array_search( 'Orders', array_column( $report_pages, 'title' ), true );
118
119 // Use array_splice to add the new item.
120 array_splice(
121 $report_pages,
122 $position + 1,
123 0,
124 array(
125 array(
126 'id' => 'woocommerce-analytics-pos',
127 'title' => /* translators: WordPress admin menu label for WCPOS. */ __( 'POS', 'woocommerce-pos' ),
128 'parent' => 'woocommerce-analytics',
129 'path' => '/analytics/pos',
130 'nav_args' => array(
131 'order' => 45,
132 'parent' => 'woocommerce-analytics',
133 ),
134 ),
135 )
136 );
137
138 return $report_pages;
139 }
140
141 /**
142 * Add POS to Admin sidebar.
143 */
144 private function register_pos_admin(): void {
145 $this->toplevel_screen_id = add_menu_page(
146 /* translators: WordPress admin menu label for WCPOS. */
147 __( 'POS', 'woocommerce-pos' ),
148 /* translators: WordPress admin menu label for WCPOS. */
149 __( 'POS', 'woocommerce-pos' ),
150 'manage_woocommerce_pos',
151 PLUGIN_NAME,
152 array( $this, 'display_upgrade_page' ),
153 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjYwIDEyNjAiPgo8cGF0aCBmaWxsPSIjYTdhYWFkIiBkPSJNMTE3MCwwaC05MEg5MDBINzIwSDU0MEgzNjBIMTgwSDkwQzMwLDAsMCwzMCwwLDkwdjE4MGMwLDQ5LjcsNDAuMyw5MCw5MCw5MHM5MC00MC4zLDkwLTkwVjkwaDE4MHYxODAKCWMwLDQ5LjcsNDAuMyw5MCw5MCw5MHM5MC00MC4zLDkwLTkwVjkwaDE4MHYxODBjMCw0OS43LDQwLjMsOTAsOTAsOTBzOTAtNDAuMyw5MC05MFY5MGgxODB2MTgwYzAsNDkuNyw0MC4zLDkwLDkwLDkwczkwLTQwLjMsOTAtOTAKCVY5MEMxMjYwLDMwLDEyMzAsMCwxMTcwLDB6Ii8+CjxwYXRoIGZpbGw9IiNhN2FhYWQiIGQ9Ik0xMDgwLDM2MGMtNDUsNDUtMTM1LDQ1LTE4MCwwYy00NSw0NS0xMzUsNDUtMTgwLDBjLTQ1LDQ1LTEzNSw0NS0xODAsMGMtNDUsNDUtMTM1LDQ1LTE4MCwwYy00NSw0NS0xMzUsNDUtMTgwLDAKCWMtNDUsNDUtMTM1LDQ1LTE4MCwwdjkwMGwzNjAtMjcwaDgxMGM2MCwwLDkwLTMwLDkwLTkwVjM2MEMxMjE1LDQwNSwxMTI1LDQwNSwxMDgwLDM2MHogTTI2MC41LDgyOGMtMzUuNSwwLTY4LjUtMTEuMy05NS41LTMwLjQKCXYxMjUuOWMwLDE5LjMtMTUuNywzNS0zNSwzNXMtMzUtMTUuNy0zNS0zNVY1MzJjMC0xOS4zLDE1LjctMzUsMzUtMzVjMTcuOCwwLDMyLjYsMTMuMywzNC43LDMwLjZjMjcuMS0xOS4zLDYwLjEtMzAuNiw5NS44LTMwLjYKCWM5MS4zLDAsMTY1LjUsNzQuMiwxNjUuNSwxNjUuNVMzNTEuOCw4MjgsMjYwLjUsODI4eiBNNjMwLDgyOGMtOTEuNSwwLTE2Ni03NC41LTE2Ni0xNjZjMC05MS41LDc0LjUtMTY2LDE2Ni0xNjYKCWM5MS41LDAsMTY2LDc0LjUsMTY2LDE2NkM3OTYsNzUzLjUsNzIxLjUsODI4LDYzMCw4Mjh6IE05MTguMyw2MTMuOWMxMS41LDUuOCwzNS4xLDEyLjYsODIuMiwxMi42YzQ5LjUsMCw4Ni42LDYuNSwxMTMuNSwyMAoJYzMzLjUsMTYuOCw1Miw0NS4zLDUyLDgwLjJzLTE4LjUsNjMuNS01Miw4MC4yYy0yNi45LDEzLjUtNjQuMSwyMC0xMTMuNSwyMEM5NDYsODI3LDg5Niw4MTMuNiw4NTIsNzg3LjJjLTE2LjYtOS45LTIyLTMxLjQtMTItNDgKCWM5LjktMTYuNiwzMS40LTIyLDQ4LTEyYzMzLDE5LjgsNzAuOCwyOS44LDExMi41LDI5LjhjNDcuMSwwLDcwLjctNi45LDgyLjItMTIuNmM3LjMtMy42LDEzLjMtNy41LDEzLjMtMTcuNnMtNi0xNC0xMy4zLTE3LjYKCWMtMTEuNS01LjgtMzUuMS0xMi42LTgyLjItMTIuNmMtNDkuNSwwLTg2LjYtNi41LTExMy41LTIwYy0zMy41LTE2LjgtNTItNDUuMy01Mi04MC4yYzAtMzUsMTguNS02My41LDUyLTgwLjIKCWMyNi45LTEzLjUsNjQuMS0yMCwxMTMuNS0yMGM1NC41LDAsMTA0LjUsMTMuNCwxNDguNSwzOS44YzE2LjYsOS45LDIyLDMxLjQsMTIsNDhjLTkuOSwxNi42LTMxLjQsMjEuOS00OCwxMgoJYy0zMy0xOS44LTcwLjgtMjkuOC0xMTIuNS0yOS44Yy00Ny4xLDAtNzAuNyw2LjktODIuMiwxMi42Yy03LjMsMy42LTEzLjMsNy41LTEzLjMsMTcuNlM5MTEsNjEwLjMsOTE4LjMsNjEzLjl6Ii8+CjxjaXJjbGUgZmlsbD0iI2E3YWFhZCIgY3g9IjYzMCIgY3k9IjY2MiIgcj0iOTYiLz4KPGNpcmNsZSBmaWxsPSIjYTdhYWFkIiBjeD0iMjYwLjUiIGN5PSI2NjIuNSIgcj0iOTUuNSIvPgo8L3N2Zz4K'
154 );
155
156 $this->view_pos_screen_id = add_submenu_page(
157 PLUGIN_NAME,
158 /* translators: WordPress admin menu label for WCPOS. */
159 __( 'View POS', 'woocommerce-pos' ),
160 /* translators: WordPress admin menu label for WCPOS. */
161 __( 'View POS', 'woocommerce-pos' ),
162 'manage_woocommerce_pos',
163 PLUGIN_NAME . '-view-pos',
164 );
165
166 $this->settings_screen_id = add_submenu_page(
167 PLUGIN_NAME,
168 // translators: WordPress admin submenu label that opens WCPOS settings.
169 __( 'Settings', 'woocommerce-pos' ),
170 // translators: WordPress admin submenu label that opens WCPOS settings.
171 __( 'Settings', 'woocommerce-pos' ),
172 'manage_woocommerce_pos',
173 PLUGIN_NAME . '-settings',
174 array( '\WCPOS\WooCommercePOS\Admin\Settings', 'display_settings_page' )
175 );
176
177 // Template Gallery SPA page.
178 $this->gallery_screen_id = add_submenu_page(
179 PLUGIN_NAME,
180 /* translators: WordPress admin menu label for WCPOS. */
181 __( 'Templates', 'woocommerce-pos' ),
182 /* translators: WordPress admin menu label for WCPOS. */
183 __( 'Templates', 'woocommerce-pos' ),
184 'manage_woocommerce_pos',
185 'wcpos-templates',
186 array( $this, 'render_gallery_page' )
187 );
188 add_action( 'load-' . $this->gallery_screen_id, array( $this, 'enqueue_gallery_assets' ) );
189
190 // adjust submenu.
191 global $submenu;
192 $pos_submenu = &$submenu[ PLUGIN_NAME ];
193 $pos_submenu[0][0] = /* translators: WordPress admin menu label for WCPOS. */ __( 'Upgrade to Pro', 'woocommerce-pos' );
194 $pos_submenu[0][2] = self::get_upgrade_tracking_url(
195 'menu_submenu',
196 admin_url( 'admin.php?page=' . PLUGIN_NAME )
197 );
198 $pos_submenu[1][2] = woocommerce_pos_url();
199
200 // The "Upgrade to Pro" submenu link is persistent navigation rendered
201 // on every wp-admin page, so we deliberately do NOT emit an
202 // `upgrade_cta_viewed` impression here — doing so fired the event on
203 // every admin request and buried the rest of the funnel. Clicks are
204 // still tracked via the tracking URL set on $pos_submenu[0][2] above.
205
206 /*
207 * Fires after POS admin menus are registered.
208 *
209 * The array arguments, `$this->toplevel_screen_id` and
210 * `$this->settings_screen_id`, refers to the top-level POS menu ID and
211 * settings submenu ID respectively.
212 *
213 * @since 1.0.0
214 *
215 * @param array $menus {
216 * An array of admin menu IDs.
217 *
218 * @type string $toplevel The top-level POS menu ID.
219 * @type string $settings The settings submenu ID.
220 * }
221 */
222 do_action(
223 'woocommerce_pos_register_pos_admin',
224 array(
225 'toplevel' => $this->toplevel_screen_id,
226 'settings' => $this->settings_screen_id,
227 )
228 );
229 }
230
231 /**
232 * Enqueue landing page scripts and styles.
233 *
234 * @param string $hook_suffix The current admin page hook suffix.
235 */
236 public function enqueue_landing_scripts_and_styles( $hook_suffix ): void {
237 if ( $hook_suffix === $this->toplevel_screen_id ) {
238 $analytics = Analytics::instance();
239
240 $analytics->capture_once(
241 'upgrade_cta_viewed',
242 array(
243 'placement' => 'admin_landing_banner',
244 ),
245 'admin_landing_banner'
246 );
247
248 // Bind this install to its `site` group and refresh the profile that
249 // hangs off it. Sending the properties matters: a bare group() call
250 // creates the group with an empty property set, which is what made
251 // every environment and store-size breakdown unqueryable (#793).
252 ( new Lifecycle_Events() )->maybe_refresh_group_properties();
253
254 // The activation funnel's middle step, carrying the feature-adoption
255 // snapshot. One snapshot per view answers "what share of stores
256 // enable X" without an event per settings toggle.
257 $analytics->capture(
258 'admin_landing_viewed',
259 array( 'settings_summary' => ( new Analytics_Profile() )->get_settings_summary() )
260 );
261
262 $is_development = isset( $_ENV['DEVELOPMENT'] )
263 && wp_validate_boolean( sanitize_text_field( wp_unslash( $_ENV['DEVELOPMENT'] ) ) );
264 $url = $is_development ? 'http://localhost:9000/' : 'https://cdn.jsdelivr.net/gh/wcpos/wp-admin-landing@2/assets/';
265
266 // Enqueue the landing page CSS from CDN.
267 wp_enqueue_style(
268 'wcpos-welcome',
269 $url . 'css/welcome.css',
270 array(),
271 PLUGIN_VERSION
272 );
273
274 // Ensure WordPress bundled React and lodash are loaded as dependencies.
275 wp_enqueue_script( 'react' );
276 wp_enqueue_script( 'lodash' );
277
278 // Enqueue the landing page JS from CDN, with React and lodash as dependencies.
279 wp_enqueue_script(
280 'wcpos-welcome',
281 $url . 'js/welcome.js',
282 array(
283 'react',
284 'react-dom',
285 'wp-element',
286 'lodash',
287 ),
288 PLUGIN_VERSION,
289 true
290 );
291
292 // Inject functional data (locale, version, anon_id, experiment
293 // bootstrap flags). The landing bundle owns PostHog initialisation,
294 // flag resolution, and identify ordering (flag-before-identify, spec
295 // §5.1); the plugin must NOT init PostHog or identify here — doing so
296 // shares the bundle's localStorage identity, breaks the anon-bucket
297 // exposure, and double-fires CTA tracking.
298 wp_add_inline_script( 'wcpos-welcome', $this->landing_inline_script(), 'before' );
299 }
300 }
301
302 /**
303 * Generate the inline script that exposes the analytics client.
304 *
305 * When the user has explicitly allowed tracking, loads the PostHog
306 * async SDK, initializes it with the configured token/host, and
307 * identifies the current user + site.
308 *
309 * When consent has not been granted, exposes a no-op stub at
310 * `window.wcpos.posthog` so that future UI event helpers can call
311 * `.capture()` etc. unconditionally without throwing — and without
312 * any network traffic leaving the browser.
313 */
314 public static function get_posthog_inline_script(): string {
315 $analytics = Analytics::instance();
316 $noop_stub = '(function(){var w=window.wcpos=window.wcpos||{};w.posthog={capture:function(){},identify:function(){},group:function(){},register:function(){},reset:function(){},opt_in_capturing:function(){},opt_out_capturing:function(){}};})();';
317
318 if ( ! $analytics->is_enabled() ) {
319 return $noop_stub;
320 }
321
322 $token = wp_json_encode( $analytics->get_token() );
323 $host = wp_json_encode( $analytics->get_host() );
324 $site_id = wp_json_encode( $analytics->get_site_id() );
325 $user_id = wp_json_encode( $analytics->get_distinct_id() );
326
327 // If any value fails to encode (e.g. malformed UTF-8 coming
328 // from a filter override), fall back to the no-op stub rather
329 // than emitting `posthog.init(, { api_host: , ... })`.
330 if ( false === $token || false === $host || false === $site_id || false === $user_id ) {
331 return $noop_stub;
332 }
333
334 // phpcs:disable Generic.Files.LineLength.TooLong -- PostHog snippet is a single line by design.
335 $snippet = <<<JS
336 (function() {
337 var wcpos = window.wcpos = window.wcpos || {};
338 !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
339 function stripUrlProperties(properties) {
340 if (!properties) {
341 return;
342 }
343
344 delete properties['\$current_url'];
345 delete properties['\$host'];
346 delete properties['\$pathname'];
347 delete properties['\$referrer'];
348 delete properties['\$referring_domain'];
349 delete properties['\$initial_current_url'];
350 delete properties['\$initial_host'];
351 delete properties['\$initial_pathname'];
352 delete properties['\$initial_referrer'];
353 delete properties['\$initial_referring_domain'];
354
355 Object.keys(properties).forEach(function(key) {
356 if (key.indexOf('\$session_entry_') === 0 || key.indexOf('\$initial_session_entry_') === 0) {
357 delete properties[key];
358 }
359 });
360 }
361
362 posthog.init(%TOKEN%, {
363 api_host: %HOST%,
364 capture_pageview: false,
365 autocapture: false,
366 persistence: 'localStorage+cookie',
367 disable_session_recording: true,
368 before_send: function(event) {
369 if (!event) {
370 return event;
371 }
372
373 stripUrlProperties(event.properties);
374 stripUrlProperties(event['\$set']);
375 stripUrlProperties(event['\$set_once']);
376
377 return event;
378 }
379 });
380 wcpos.posthog = posthog;
381 var distinctId = %USER_ID%;
382 var siteId = %SITE_ID%;
383 if (distinctId) { posthog.identify(distinctId); }
384 if (siteId) { posthog.group('site', siteId); }
385 })();
386 JS;
387 // phpcs:enable Generic.Files.LineLength.TooLong
388
389 return str_replace(
390 array( '%TOKEN%', '%HOST%', '%USER_ID%', '%SITE_ID%' ),
391 array( $token, $host, $user_id, $site_id ),
392 $snippet
393 );
394 }
395
396 /**
397 * Build an admin-post URL that tracks an upgrade click before redirecting.
398 *
399 * @param string $placement Stable CTA placement identifier.
400 * @param string $destination Final redirect URL.
401 *
402 * @return string
403 */
404 public static function get_upgrade_tracking_url( string $placement, string $destination ): string {
405 return add_query_arg(
406 array(
407 'action' => 'wcpos_track_upgrade_click',
408 'placement' => $placement,
409 'destination' => $destination,
410 '_wpnonce' => wp_create_nonce( 'wcpos_track_upgrade_click' ),
411 ),
412 admin_url( 'admin-post.php' )
413 );
414 }
415
416 /**
417 * Track an upgrade click and return a safe redirect destination.
418 *
419 * @param string $placement Stable CTA placement identifier.
420 * @param string $destination Final redirect URL.
421 *
422 * @return string
423 */
424 public static function track_upgrade_click( string $placement, string $destination ): string {
425 $safe_destination = self::sanitize_upgrade_destination( $destination );
426
427 Analytics::instance()->capture(
428 'pro_link_clicked',
429 array(
430 'placement' => sanitize_key( $placement ),
431 'destination' => $safe_destination,
432 )
433 );
434
435 return $safe_destination;
436 }
437
438 /**
439 * Handle admin-post upgrade click tracking and redirect.
440 */
441 public static function handle_upgrade_click_redirect(): void {
442 check_admin_referer( 'wcpos_track_upgrade_click' );
443
444 if ( ! current_user_can( 'manage_woocommerce_pos' ) ) {
445 wp_die( esc_html__( 'You do not have permission to access this page.', 'woocommerce-pos' ) );
446 }
447
448 $placement = isset( $_GET['placement'] ) ? sanitize_text_field( wp_unslash( $_GET['placement'] ) ) : '';
449 $destination = isset( $_GET['destination'] ) ? sanitize_text_field( wp_unslash( $_GET['destination'] ) ) : '';
450 $redirect_to = self::track_upgrade_click( $placement, $destination );
451
452 wp_safe_redirect( $redirect_to );
453 exit;
454 }
455
456 /**
457 * Generate the inline script for landing page data.
458 *
459 * Always emits functional data (locale, version, pro status).
460 * Merges in store profile and updates-server config only when
461 * the user has explicitly allowed tracking.
462 *
463 * @return string
464 */
465 private function landing_inline_script(): string {
466 $json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
467 $profile = new Landing_Profile();
468 $data = $profile->get_functional_data();
469
470 $consent = Settings::instance()->tracking_consent();
471 if ( 'allowed' === $consent ) {
472 $data = array_merge( $data, $profile->get_consented_data() );
473 }
474
475 $encoded = wp_json_encode( $data, $json_flags );
476
477 if ( false === $encoded ) {
478 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
479 error_log( 'WCPOS landing data JSON encoding failed: ' . json_last_error_msg() );
480 $encoded = '{}';
481 }
482
483 return \sprintf(
484 'var wcpos = wcpos || {}; wcpos.landing = %s;',
485 $encoded
486 );
487 }
488
489 /**
490 * Print a tiny global click tracker for PHP-rendered admin upsell links.
491 */
492 public function print_upgrade_click_tracking_script(): void {
493 $nonce = wp_create_nonce( 'wcpos_track_upgrade_click' );
494 ?>
495 <script>
496 (function() {
497 if (!window.ajaxurl) {
498 return;
499 }
500
501 document.addEventListener('click', function(event) {
502 var target = event.target;
503 if (!target || !target.closest) {
504 return;
505 }
506
507 var link = target.closest('[data-wcpos-upgrade-placement]');
508 if (!link) {
509 return;
510 }
511
512 var placement = link.getAttribute('data-wcpos-upgrade-placement');
513 var destination = link.getAttribute('href');
514 if (!placement || !destination || !window.navigator || !window.navigator.sendBeacon) {
515 return;
516 }
517
518 var data = new URLSearchParams();
519 data.set('action', 'wcpos_track_upgrade_click_ajax');
520 data.set('placement', placement);
521 data.set('destination', destination);
522 data.set('_ajax_nonce', '<?php echo esc_js( $nonce ); ?>');
523 window.navigator.sendBeacon(window.ajaxurl, data);
524 });
525 })();
526 </script>
527 <?php
528 }
529
530 /**
531 * AJAX handler for admin upgrade click tracking.
532 */
533 public static function handle_upgrade_click_ajax(): void {
534 check_ajax_referer( 'wcpos_track_upgrade_click' );
535
536 if ( ! current_user_can( 'manage_woocommerce_pos' ) ) {
537 wp_send_json_error( 'Unauthorized', 403 );
538 }
539
540 $placement = isset( $_POST['placement'] ) ? sanitize_text_field( wp_unslash( $_POST['placement'] ) ) : '';
541 $destination = isset( $_POST['destination'] ) ? sanitize_text_field( wp_unslash( $_POST['destination'] ) ) : '';
542
543 self::track_upgrade_click( $placement, $destination );
544
545 wp_send_json_success();
546 }
547
548 /**
549 * Render the Template Gallery SPA mount point.
550 */
551 public function render_gallery_page(): void {
552 echo '<div class="wrap"><div id="wcpos-template-gallery"></div></div>';
553 }
554
555 /**
556 * Enqueue the Template Gallery SPA assets.
557 */
558 public function enqueue_gallery_assets(): void {
559 $is_development = isset( $_ENV['DEVELOPMENT'] )
560 && wp_validate_boolean( sanitize_text_field( wp_unslash( $_ENV['DEVELOPMENT'] ) ) );
561 $dir = $is_development ? 'build' : 'assets';
562
563 wp_enqueue_style(
564 'wcpos-template-gallery-styles',
565 PLUGIN_URL . $dir . '/css/template-gallery.css',
566 array(),
567 PLUGIN_VERSION
568 );
569
570 wp_enqueue_script(
571 'wcpos-template-gallery',
572 PLUGIN_URL . $dir . '/js/template-gallery.js',
573 array( 'react', 'react-dom', 'wp-api-fetch', 'wp-url' ),
574 PLUGIN_VERSION,
575 true
576 );
577
578 wp_add_inline_script( 'wcpos-template-gallery', $this->gallery_inline_script(), 'before' );
579 }
580
581 /**
582 * Generate the inline script for gallery data.
583 */
584 private function gallery_inline_script(): string {
585 $json_encode_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
586
587 return \sprintf(
588 'var wcpos = wcpos || {}; wcpos.templateGallery = { isProActive: %s, adminUrl: %s, hasPosOrders: %s, previewBaseUrl: %s }; wcpos.translationVersion = %s;',
589 wp_json_encode( class_exists( '\WCPOS\WooCommercePOSPro\WooCommercePOSPro' ), $json_encode_flags ),
590 wp_json_encode( untrailingslashit( admin_url() ), $json_encode_flags ),
591 wp_json_encode(
592 (bool) wc_get_orders(
593 array(
594 'limit' => 1,
595 'return' => 'ids',
596 'status' => array( 'completed', 'processing', 'on-hold', 'pending' ),
597 'created_via' => 'woocommerce-pos',
598 )
599 ),
600 $json_encode_flags
601 ),
602 wp_json_encode( PLUGIN_URL . 'assets/img/template-gallery/previews', $json_encode_flags ),
603 wp_json_encode( TRANSLATION_VERSION, $json_encode_flags )
604 );
605 }
606
607 /**
608 * Redirect the old CPT list page (edit.php?post_type=wcpos_template) to the Gallery SPA.
609 *
610 * Only redirects the list view, not the individual post editor.
611 */
612 public function redirect_template_list_page(): void {
613 global $pagenow;
614
615 if (
616 'edit.php' === $pagenow
617 && isset( $_GET['post_type'] )
618 && 'wcpos_template' === $_GET['post_type']
619 && ! isset( $_GET['post_status'] )
620 ) {
621 wp_safe_redirect( admin_url( 'admin.php?page=wcpos-templates' ) );
622 exit;
623 }
624 }
625
626 /**
627 * Keep the POS menu expanded and Templates submenu highlighted when editing a template.
628 *
629 * @param string $parent_file The parent file.
630 *
631 * @return string
632 */
633 public function highlight_templates_menu( $parent_file ) {
634 global $current_screen, $submenu_file;
635
636 if ( isset( $current_screen->post_type ) && 'wcpos_template' === $current_screen->post_type ) {
637 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
638 $submenu_file = 'wcpos-templates';
639 $parent_file = PLUGIN_NAME;
640 }
641
642 return $parent_file;
643 }
644
645 /**
646 * Sanitize an upgrade redirect destination to trusted hosts only.
647 *
648 * @param string $destination Candidate destination URL.
649 *
650 * @return string
651 */
652 private static function sanitize_upgrade_destination( string $destination ): string {
653 $fallback = 'https://wcpos.com/pro';
654 $destination = rawurldecode( $destination );
655 $parsed_url = wp_parse_url( $destination );
656 $parsed_admin = wp_parse_url( admin_url() );
657 $allowed_hosts = array_filter(
658 array(
659 $parsed_admin['host'] ?? '',
660 'wcpos.com',
661 'www.wcpos.com',
662 )
663 );
664
665 if ( empty( $parsed_url['host'] ) || ! \in_array( $parsed_url['host'], $allowed_hosts, true ) ) {
666 return $fallback;
667 }
668
669 return esc_url_raw( $destination );
670 }
671 }
672