1// Capture a SureFeedback/ProjectHuddle access token from a magic-link URL 2// (?ph_access_token=â¦) into localStorage so the injected tool can authenticate. 3// Previously this param was ignored, so portal "share"/access links never 4// carried through and the toolbar stayed unauthenticated ("not allowed"). 5(function () { 6 var t = new URLSearchParams(window.location.search).get('ph_access_token'); 7 if (t) { 8 localStorage.setItem('ph_access_token', t); 9 console.log('[StickyDevice] Captured ph_access_token from URL'); 10 } 11})(); 12 13// Check if device already registered 14const existingDeviceId = localStorage.getItem('sticky_device_id'); 15// A portal "Project Access Link" sends visitors to the client site with 16// ?ph_access_token=⦠and NO ?note. Gating on 'note' alone meant those links hit 17// neither branch below: no toolbar, no consent modal, no error â a dead page 18// for anyone following the link the portal itself hands out. Treat an access 19// token as an equally valid way into the consent flow. 20// (Safe: the modal markup is always rendered, and registerUrl hardcodes 21// note=1, so the REST consent check still passes.) 22const stickyParams = new URLSearchParams(window.location.search); 23const hasStickyParam = stickyParams.has('note') || stickyParams.has('ph_access_token'); 24 25if (existingDeviceId) { 26 const projectId = StickyDeviceVars.projectId; 27 const apiKey = StickyDeviceVars.apiKey; 28 const stickyNoteUrl = StickyDeviceVars.stickyNoteUrl; 29 console.log('[StickyDevice] Existing device ID found:', existingDeviceId); 30 injectStickyNoteScript( 31 stickyNoteUrl, 32 projectId, 33 apiKey 34 ); 35 submitDeviceToPortal(true); // Send heartbeat/update to portal 36} else if (hasStickyParam) { 37 console.log('[StickyDevice] No existing device ID found'); 38 setTimeout(function () { 39 MicroModal.show('modal-1'); 40 }, 100); 41} 42 43function registerDevice() { 44 // Fetch a new device ID from plugin REST endpoint. 45 // Returns the fetch promise so the caller can await before reloading â 46 // critical to prevent the "consent popup keeps appearing" bug where the 47 // page reloaded before localStorage.setItem ran. 48 console.log('[StickyDevice] registerUrl:', StickyDeviceVars.registerUrl); 49 50 const headers = { 51 'Content-Type': 'application/json', 52 }; 53 54 // Add nonce for logged-in users. 55 // 56 // This used to send the nonce to EVERYONE, which broke consent on every 57 // full-page-cached site. wp_create_nonce('wp_rest') is baked into the HTML, 58 // so a cached page hands the same frozen nonce to every visitor. WordPress 59 // core's rest_cookie_check_errors() verifies ANY nonce it receives â 60 // logged in or not â and runs BEFORE our permission_callback, so a stale or 61 // wrong-user nonce turned into a hard 403 rest_cookie_invalid_nonce that the 62 // plugin could never see, let alone recover from. 63 // 64 // Anonymous visitors don't need it: the endpoint's permission_callback 65 // authorises them on the consent param alone. Sending it could only ever 66 // cost us a 403, so don't. 67 if (StickyDeviceVars.nonce && StickyDeviceVars.isLoggedIn) { 68 headers['X-WP-Nonce'] = StickyDeviceVars.nonce; 69 } 70 71 return fetch(StickyDeviceVars.registerUrl, { 72 method: 'GET', 73 credentials: 'include', 74 headers: headers 75 }) 76 .then(async res => { 77 const contentType = res.headers.get('content-type') || ''; 78 79 if (!res.ok) { 80 const text = await res.text(); 81 console.error('[StickyDevice] Register request failed:', res.status, res.statusText); 82 console.error('[StickyDevice] Response body:', text); 83 throw new Error(`HTTP ${res.status}: ${res.statusText}`); 84 } 85 86 if (!contentType.includes('application/json')) { 87 const text = await res.text(); 88 console.error('[StickyDevice] Expected JSON but got content-type:', contentType); 89 console.error('[StickyDevice] Response body:', text); 90 throw new Error('Invalid content-type: expected JSON'); 91 } 92 93 return res.json(); 94 }) 95 .then(data => { 96 console.log('[StickyDevice] Response data:', data); 97 98 if (!data.deviceId) { 99 console.error('[StickyDevice] No device ID in response'); 100 throw new Error('No device ID in response'); 101 } 102 103 // Save ID in localStorage 104 localStorage.setItem('sticky_device_id', data.deviceId); 105 localStorage.setItem('sticky_device_url', data.url); 106 // console.log('[StickyDevice] Device ID saved:', data.deviceId); 107
108 // Remove consent parameter from URL 109 // const url = new URL(window.location); 110 // url.searchParams.delete('sticky_note_consent'); 111 // window.history.replaceState({}, '', url); 112 }) 113 .catch(err => { 114 // Log, then RE-THROW. Swallowing this is what made the bug 115 // invisible: the caller reloaded the page regardless, so a failed 116 // registration looked exactly like a successful one that simply 117 // hadn't drawn the toolbar yet. The caller needs to know. 118 console.error('[StickyDevice] Error fetching device ID:', err); 119 throw err; 120 }); 121} 122 123function injectStickyNoteScript() { 124 console.log('[StickyDevice] Injecting sticky note script'); 125 const url = localStorage.getItem('sticky_device_url'); 126 const phAccessToken = localStorage.getItem('ph_access_token'); 127 if (!url) { 128 console.error('[StickyDevice] No sticky device URL found in localStorage'); 129 return; 130 } 131 const script = document.createElement('script'); 132 script.type = 'text/javascript'; 133 script.async = true; 134 script.defer = true; 135 script.charset = 'UTF-8'; 136 script.src = url + '&v=' + (new Date()).getTime(); 137 if (phAccessToken) {
138 script.src += '&ph_access_token=' + encodeURIComponent(phAccessToken); 139 } 140 const firstScript = document.getElementsByTagName('script')[0]; 141 firstScript.parentNode.insertBefore(script, firstScript); 142} 143 144// Inject sticky note script if URL param indicates consent 145 146// Utility function to get current device ID 147function getStickyDeviceId() { 148 return localStorage.getItem('sticky_device_id'); 149} 150 151/** 152 * const deviceRegistration = new DeviceRegistration({ 153 apiEndpoint: 'https://your-wordpress-site.com/wp-json/sntd/v1/register-device', 154 apiKey: 'your-api-key-here', 155 debug: true 156}); 157 158// Register device when user logs in or visits 159deviceRegistration.registerDevice('[email protected]', 123) 160 .then(result => { 161 if (result.success) { 162 console.log('Device registered:', result); 163 164 // Start heartbeat to keep device active 165 deviceRegistration.startHeartbeat('[email protected]', 15); 166 } 167 }); 168 169// Or use auto-register (checks if already registered recently) 170deviceRegistration.autoRegister('[email protected]', 123); 171 */ 172 173async function submitDeviceToPortal(isUpdate = false) { 174 const { restApiKey, email: configuredEmail = '', portalUrl, projectId = 0 } = StickyDeviceVars.config; 175 176 const deviceRegistration = new DeviceRegistration({ 177 apiEndpoint: `${portalUrl}/wp-json/sntd/v1/register-device`, 178 apiKey: restApiKey, 179 debug: true 180 }); 181 182 const getEmail = async () => { 183 if (configuredEmail) { 184 return configuredEmail; 185 } 186 187 // An anonymous client-site visitor never has a portal session, so the 188 // cross-origin /current-user call just returns 401 after ~1s of 189 // CORS-preflight + round-trip. Skip it entirely unless logged in â 190 // this was a major contributor to the Accept-button lag. 191 if (!StickyDeviceVars.isLoggedIn) { 192 return ''; 193 } 194 195 try { 196 const response = await fetch(`${portalUrl}/wp-json/sntd/v1/current-user`, { 197 credentials: 'include' 198 }); 199 const data = await response.json(); 200 return data?.data?.email || ''; 201 } catch (error) { 202 console.error('[StickyDevice] Failed to fetch current user:', error); 203 return ''; 204 } 205 }; 206 207 const userEmail = await getEmail(); 208 209 if (isUpdate) { 210 if (!userEmail) { 211 console.log('[StickyDevice] Cannot send heartbeat without email'); 212 return; 213 } 214 215 console.log('[StickyDevice] Sending heartbeat for:', userEmail); 216 // Start heartbeat to keep device active 217 deviceRegistration.startHeartbeat(userEmail, 15); 218 return; 219 } 220 221 try { 222 const result = await deviceRegistration.registerDevice(userEmail, projectId); 223 224 if (result.success) { 225 console.log('[StickyDevice] Device registered to portal successfully:', result); 226 if (!userEmail) { 227 console.log('[StickyDevice] Cannot send heartbeat without email'); 228 return; 229 } 230 // Start heartbeat to keep device active 231 deviceRegistration.startHeartbeat(userEmail, 15); 232 } else { 233 console.error('[StickyDevice] Registration to portal failed:', result.error); 234 } 235 } catch (error) { 236 console.error('[StickyDevice] Device registration error:', error); 237 } 238} 239 240// Expose function globally for other scripts 241window.getStickyDeviceId = getStickyDeviceId;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.