| @@ -9,11 +9,15 @@ | ||
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | namespace WCPOS\WooCommercePOS\Templates; |
| 12 | 12 | |
| 13 | -use Ramsey\Uuid\Uuid; | |
| 14 | 13 | use WCPOS\WooCommercePOS\Services\Auth; |
| 14 | +use WCPOS\WooCommercePOS\Services\Cashier; | |
| 15 | +use WCPOS\WooCommercePOS\Services\Lifecycle_Events; | |
| 16 | +use WCPOS\WooCommercePOS\Services\Settings; | |
| 17 | +use WCPOS\WooCommercePOS\Sync\Pos_Uuid; | |
| 15 | 18 | use WCPOS\WooCommercePOS\Template_Router; |
| 19 | +use const WCPOS\WooCommercePOS\PLUGIN_PATH; | |
| 16 | 20 | use const WCPOS\WooCommercePOS\PLUGIN_URL; |
| 17 | 21 | use const WCPOS\WooCommercePOS\SHORT_NAME; |
| 18 | 22 | use const WCPOS\WooCommercePOS\VERSION; |
| 19 | 23 | |
| @@ -38,9 +42,9 @@ | ||
| 38 | 42 | * @return void |
| 39 | 43 | */ |
| 40 | 44 | public function get_template(): void { |
| 41 | 45 | // force ssl. |
| 42 | - if ( ! is_ssl() && woocommerce_pos_get_settings( 'general', 'force_ssl' ) ) { | |
| 46 | + if ( ! is_ssl() && Settings::instance()->force_ssl_enabled() ) { | |
| 43 | 47 | wp_safe_redirect( woocommerce_pos_url() ); |
| 44 | 48 | exit; |
| 45 | 49 | } |
| 46 | 50 | |
| @@ -50,11 +54,11 @@ | ||
| 50 | 54 | auth_redirect(); |
| 51 | 55 | } |
| 52 | 56 | |
| 53 | 57 | // check privileges. |
| 54 | - if ( ! current_user_can( 'access_woocommerce_pos' ) ) { | |
| 55 | - // translators: Authorization error shown when a logged-in user lacks permission to open the POS page. | |
| 56 | - wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'woocommerce-pos' ) ); | |
| 58 | + $user = wp_get_current_user(); | |
| 59 | + if ( ! Cashier::instance()->can_open_pos( $user ) ) { | |
| 60 | + wp_die( esc_html( Cashier::instance()->missing_pos_capabilities_message( $user ) ), '', array( 'response' => 403 ) ); | |
| 57 | 61 | } |
| 58 | 62 | |
| 59 | 63 | // disable cache plugins. |
| 60 | 64 | $this->no_cache(); |
| @@ -80,8 +84,17 @@ | ||
| 80 | 84 | $user = wp_get_current_user(); |
| 81 | 85 | $auth_service = Auth::instance(); |
| 82 | 86 | $this->wp_credentials = $auth_service->get_user_data( $user, true ); |
| 83 | 87 | |
| 88 | + // The activation funnel's step the admin side cannot see: the POS itself | |
| 89 | + // being opened. Recorded here rather than by tracking the menu link, so | |
| 90 | + // a bookmark, a direct URL or a till that never touches wp-admin all | |
| 91 | + // count — and so it counts opens, not clicks that may never arrive. | |
| 92 | + // | |
| 93 | + // Everything above has already established that this is a logged-in user | |
| 94 | + // with `access_woocommerce_pos`, past the SSL redirect. | |
| 95 | + ( new Lifecycle_Events() )->report_app_opened(); | |
| 96 | + | |
| 84 | 97 | include woocommerce_pos_locate_template( 'pos.php' ); |
| 85 | 98 | exit; |
| 86 | 99 | } |
| 87 | 100 | |
| @@ -142,8 +155,18 @@ | ||
| 142 | 155 | |
| 143 | 156 | // Default to the plugin's own major.minor so the stable lane tracks the |
| 144 | 157 | // version automatically: a 1.9.x plugin loads `@1.9`, a 1.10.x plugin loads |
| 145 | 158 | // `@1.10`, etc. — no edit needed as versions roll. |
| 159 | + | |
| 160 | + /* | |
| 161 | + * One jsDelivr ref per lane, named after the lane (owner ruling, 2026-09-04): | |
| 162 | + * released lane → `@<major.minor>` (this default; the tag is cut at release) | |
| 163 | + * next lane → `@next` — the `next` BRANCH of wcpos/web-bundle IS the dev | |
| 164 | + * lane's tag. There is no versioned/prerelease tag for `next`. | |
| 165 | + * dev-next sets WCPOS_WEB_BUNDLE_REF=next to load it. | |
| 166 | + */ | |
| 167 | + // When `next` becomes `main`, the released ref simply becomes the new | |
| 168 | + // major.minor (e.g. `@1.11`) via this default. | |
| 146 | 169 | $default_bundle_ref = implode( '.', \array_slice( explode( '.', VERSION ), 0, 2 ) ); |
| 147 | 170 | |
| 148 | 171 | /** |
| 149 | 172 | * The web-bundle ref served from jsDelivr (or a full base URL). |
| @@ -149,10 +172,12 @@ | ||
| 149 | 172 | * The web-bundle ref served from jsDelivr (or a full base URL). |
| 150 | 173 | * |
| 151 | 174 | * Override via the WCPOS_WEB_BUNDLE_REF constant / env var or this filter to |
| 152 | 175 | * point a site at another lane for testing the in-development build locally |
| 153 | - * or on staging: a branch (e.g. `next`), a tag, a commit, or a full base URL | |
| 154 | - * (anything containing `://`, e.g. a local dev server or an EAS preview). | |
| 176 | + * or on staging: the `next` lane's ref is the branch `next` | |
| 177 | + * (https://cdn.jsdelivr.net/gh/wcpos/web-bundle@next); a tag, a commit, or a | |
| 178 | + * full base URL (anything containing `://`, e.g. a local dev server or an EAS | |
| 179 | + * preview) also work. | |
| 155 | 180 | * |
| 156 | 181 | * @hook woocommerce_pos_web_bundle_ref |
| 157 | 182 | */ |
| 158 | 183 | $bundle_ref = (string) apply_filters( 'woocommerce_pos_web_bundle_ref', $explicit_bundle_ref ?? $default_bundle_ref ); |
| @@ -182,23 +207,21 @@ | ||
| 182 | 207 | }, |
| 183 | 208 | wcpos_get_stores() |
| 184 | 209 | ); |
| 185 | 210 | |
| 186 | - $site_uuid = get_option( 'woocommerce_pos_uuid' ); | |
| 187 | - if ( ! $site_uuid ) { | |
| 188 | - $site_uuid = Uuid::uuid4()->toString(); | |
| 189 | - update_option( 'woocommerce_pos_uuid', $site_uuid ); | |
| 211 | + $site_uuid = wcpos_get_site_uuid(); | |
| 212 | + $opfs_worker_hash = hash_file( 'sha256', PLUGIN_PATH . 'assets/js/opfs.worker.js' ); | |
| 213 | + if ( false === $opfs_worker_hash ) { | |
| 214 | + $opfs_worker_hash = VERSION; | |
| 190 | 215 | } |
| 191 | 216 | |
| 192 | - $user_uuid = get_user_meta( $user->ID, '_woocommerce_pos_uuid', true ); | |
| 193 | - if ( ! $user_uuid ) { | |
| 194 | - $user_uuid = Uuid::uuid4()->toString(); | |
| 195 | - update_user_meta( $user->ID, '_woocommerce_pos_uuid', $user_uuid ); | |
| 196 | - } | |
| 217 | + // Pos_Uuid is the sole authority for `_woocommerce_pos_uuid`: the value here | |
| 218 | + // must match what /cashier and /customers serve, or the client forks identities. | |
| 219 | + $user_uuid = Pos_Uuid::ensure_user_uuid( $user ); | |
| 197 | 220 | |
| 198 | 221 | $vars = array( |
| 199 | 222 | 'version' => VERSION, |
| 200 | - 'manifest' => $cdn_base_url . '/metadata.json?v=' . VERSION, | |
| 223 | + 'manifest' => $cdn_base_url . '/metadata.json?v=' . $opfs_worker_hash, | |
| 201 | 224 | 'homepage' => woocommerce_pos_url(), |
| 202 | 225 | 'logout_url' => $this->pos_logout_url(), |
| 203 | 226 | 'site' => array( |
| 204 | 227 | 'uuid' => $site_uuid, |
| @@ -212,9 +235,9 @@ | ||
| 212 | 235 | 'wc_version' => WC()->version, |
| 213 | 236 | 'wcpos_version' => VERSION, |
| 214 | 237 | 'wp_api_url' => get_rest_url(), |
| 215 | 238 | 'wc_api_url' => trailingslashit( get_rest_url( null, 'wc/v3' ) ), |
| 216 | - 'wcpos_api_url' => trailingslashit( get_rest_url( null, 'wcpos/v1' ) ), | |
| 239 | + 'wcpos_api_url' => trailingslashit( get_rest_url( null, 'wcpos/v2' ) ), | |
| 217 | 240 | 'wcpos_login_url' => Template_Router::get_auth_url(), |
| 218 | 241 | 'locale' => get_locale(), |
| 219 | 242 | ), |
| 220 | 243 | 'wp_credentials' => $this->wp_credentials, |
| @@ -239,9 +262,13 @@ | ||
| 239 | 262 | /** |
| 240 | 263 | * Add path to worker scripts. |
| 241 | 264 | */ |
| 242 | 265 | $idb_worker = PLUGIN_URL . 'assets/js/indexeddb.worker.js'; |
| 243 | - $opfs_worker = PLUGIN_URL . 'assets/js/opfs.worker.js'; | |
| 266 | + $opfs_worker = add_query_arg( | |
| 267 | + 'ver', | |
| 268 | + $opfs_worker_hash, | |
| 269 | + PLUGIN_URL . 'assets/js/opfs.worker.js' | |
| 270 | + ); | |
| 244 | 271 | |
| 245 | 272 | // getScript helper and initialProps. |
| 246 | 273 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline JavaScript for POS frontend |
| 247 | 274 | echo "<script> |
| @@ -283,9 +310,13 @@ | ||
| 283 | 310 | var baseUrl = '{$wcpos_base_path}'; |
| 284 | 311 | </script>" . "\n"; |
| 285 | 312 | |
| 286 | 313 | echo "<script> |
| 287 | - var request = new Request(initialProps.manifest); | |
| 314 | + // no-cache: revalidate the manifest with the CDN (ETag/304) on every boot. | |
| 315 | + // jsDelivr serves it with max-age=604800 and the ?v= buster only changes on | |
| 316 | + // plugin deploys, so a default fetch pins users to a stale bundle for up to | |
| 317 | + // 7 days after a web-bundle publish. | |
| 318 | + var request = new Request(initialProps.manifest, { cache: 'no-cache' }); | |
| 288 | 319 | |
| 289 | 320 | window.fetch(request) |
| 290 | 321 | .then(function(response) { return response.json(); }) |
| 291 | 322 | .then(function(data) { |