/*! * PHASE 6M — Nuva -> IS60 handoff token delivery (outbound link only) * * WHAT THIS FILE DOES, AND ONLY THIS: * NuvaProductCatalogAdmin.getProductUrl(product) is the single function * every "View Product" link on the Quick Skin Match / Advanced Skin Scan * results pages already goes through -- is60-bridge.js (Phase 6D) already * wraps it once to swap in the real, live IS60 URL and to append its own * non-identity utm_source/utm_medium/nuva_ref tag. This file wraps * getProductUrl() a SECOND, independent time, on top of whatever it * already returns, to additionally append the current, already-issued, * still-valid Phase 6L handoff token as one query parameter: * `is60_ho=`. It creates no token, extends no token, and reads the * token from exactly the same localStorage slot Phase 6L already writes * (`nuva_quick_skin_match_handoff_token` / `nuva_advanced_skin_scan_ * handoff_token`) -- never a new mechanism. * * Every resolved IS60 product link on the page gets the same token * appended, for as long as that token stays valid. This is deliberate: * the token is single-use, and IS60 already redeems and invalidates it on * the very first request that carries it (Phase 6K, live and unmodified), * so only the first product link a visitor actually clicks ever redeems * anything -- a later click on a different link that same session simply * carries an already-used token, which IS60's existing redemption logic * already fails open on (no error, no attribution, page loads normally). * This file does not choose a single "primary" link to favor; doing so * would be new steering logic this phase does not add. * * WHAT THIS FILE DELIBERATELY DOES NOT DO: * - It never puts consultation_ref or customer_ref in a URL, a query * string, or anywhere else -- the only new value it ever writes into a * URL is the single, opaque, already-issued handoff token itself. * - It never creates a handoff token, never asks IS60 for one, and never * extends or refreshes one -- it only reads whatever Phase 6L already * stored, exactly as-is. * - It never accepts a token from anywhere other than that one reserved * localStorage slot -- never a URL parameter, a global variable, a * postMessage, or any other page input, so nothing on the page can hand * this file a token to relay on its behalf. * - It never attempts to reuse a token after this browser believes it * has been redeemed -- it has no way to know that (redemption happens * server-side, on IS60, outside this file's visibility) and does not * try to guess; it simply keeps offering the same still-unexpired token * until Phase 6L's own localStorage slot changes or the token expires, * matching the "every eligible link may carry the current token" * product behavior above. * - It never touches is60-bridge.js -- it wraps getProductUrl() a second * time, independently, the same non-destructive way is60-bridge.js * itself wrapped it a first time. is60-bridge.js's own source is not * read, opened, or modified by this file's installation. * - It does not touch product matching, assessment, results rendering, * NuvaCart, NuvaCheckout, pricing, entitlements, or attribution logic. * Attribution is IS60's own, already-live, already-tested concern * (Phase 6I/6J/6K) that this file's only job is to make reachable. * - It never logs the token. No console output, no analytics event, no * network call of any kind -- this file only edits a URL string that * was already about to be rendered as a link. * - If no valid token exists for the current consultation type, or if * anything about reading/parsing it fails, every product URL is * returned completely unchanged -- fail open, exactly like every other * new capability added since Phase 6D. * * INTEGRATION POINT: window.NuvaProductCatalogAdmin.getProductUrl(product) * -- the same function is60-bridge.js wraps. (Not a separate "matching * engine" object; NuvaProductRecommendationUI's card-building code already * calls this one function for every rendered link, which is what makes * wrapping it here sufficient for "every View Product link" without * touching the recommendation UI itself.) * * Runs on the Quick Skin Match (free) and Advanced Skin Scan (advanced) * pages, same as Phase 6D and Phase 6L. */ (function () { 'use strict'; var TOKEN_KEY_BY_SLUG = { 'quick-skin-match': 'nuva_quick_skin_match_handoff_token', 'ai-skin-scan': 'nuva_advanced_skin_scan_handoff_token' }; // Same slug-detection approach is60-bridge.js already uses for its own // ref tag -- not a new pattern, just applied to a different lookup table. function currentTokenKey() { var slug = (window.location.pathname || '').replace(/\/+$/, '').split('/').pop(); return TOKEN_KEY_BY_SLUG[slug] || null; } // Reads the current, still-valid handoff token for this page's // consultation type, or null for any reason at all -- wrong page, // nothing stored yet, malformed JSON, missing fields, or expired. // Never throws; every branch below returns rather than raising. function getValidToken() { try { var key = currentTokenKey(); if (!key) { return null; } var raw = window.localStorage.getItem(key); if (!raw) { return null; } var parsed = JSON.parse(raw); if (!parsed || typeof parsed.token !== 'string' || !parsed.token) { return null; } if (typeof parsed.expiresAt !== 'number' || Date.now() >= parsed.expiresAt) { return null; // expired, or malformed expiry -- treat as absent either way } return parsed.token; } catch (e) { return null; // fail open -- never let a storage/parse error propagate } } // Eligibility check: only ever append the token to a URL that is // actually an IS60 product link -- never to any other host. function isIS60Url(url) { return typeof url === 'string' && /^https?:\/\/is60\.store(\/|\?|$)/i.test(url); } // Appends exactly one query parameter, `is60_ho`, using the same // separator convention is60-bridge.js's own withRefTag() already uses -- // '?' when the URL has no query string yet, '&' otherwise -- so every // existing query parameter (including is60-bridge.js's own utm_source/ // utm_medium/nuva_ref tag) is preserved untouched, in place, in order. function appendHandoffToken(url, token) { var sep = url.indexOf('?') === -1 ? '?' : '&'; return url + sep + 'is60_ho=' + encodeURIComponent(token); } function patchCatalogAdminForHandoffDelivery(admin) { if (!admin || admin.__is60HandoffDeliveryApplied) { return; } admin.__is60HandoffDeliveryApplied = true; var previousGetProductUrl = admin.getProductUrl.bind(admin); admin.getProductUrl = function (product) { // The existing link-resolution logic runs first, completely // unmodified -- its own behavior and any of its own errors are // not this file's concern and are never caught here. var url = previousGetProductUrl(product); try { if (!isIS60Url(url)) { return url; // not an IS60 link -- nothing to add } var token = getValidToken(); if (!token) { return url; // no valid token -- normal link, unchanged } return appendHandoffToken(url, token); } catch (e) { // Fail open -- a bug in this file's own token handling must // never break or alter the underlying product link. return url; } }; } function init() { var admin = window.NuvaProductCatalogAdmin; if (!admin || typeof admin.getProductUrl !== 'function') { return; // this page doesn't load the shared catalog module } patchCatalogAdminForHandoffDelivery(admin); } init(); if (document.readyState !== 'complete') { window.addEventListener('load', init); // retry once, same as is60-bridge.js } })();