1/** 2 * realtimeOrderUpdates.js â push-driven "refresh now" for the past-order tracking 3 * page, in place of waiting up to 30s for the status poll. 4 * 5 * Hands the raw event to the caller and interprets nothing: the vendor speaks 6 * 'PREPARED', this codebase numeric codes, and pastOrders.js owns that mapping. 7 * All SDK knowledge stays in this file. 8 * 9 * Fails silent, always â the 30s poll in pastOrders.js is the fallback for every 10 * failure below, and a page that renders beats a push that arrives. 11 * 12 * Auth: backend mints a short-lived, ORDER-SCOPED token and ships it inline on 13 * the client/cart response (`realtimeToken` / `realtimeChannel` / 14 * `realtimeTokenExpiresIn`). The predecessor put a business-issuer token in the 15 * page; it was not tenant-scoped â verified 2026-09-08, ours returned 200 for 16 * orders in tenants 49859 and 6353 â and order ids are sequential, so devtools 17 * bought any brand's orders. Don't reintroduce a page-held business token. 18 * 19 * Doc: docs/Realtime-Order-Updates-SDK.md 20 * 21 * Cache: nginx holds /src/js for 30 days and nested imports are unversioned, so 22 * a later edit here can serve stale. Bump the ?v= on pastOrders.js's import when 23 * a change must land hard. 24 */ 25 26// 2.0.0 is required, not housekeeping: 1.0.0 has no `toChannelPath`, so it put 27// backend's bare channel id ("48427947") into the subscribe frame and AppSync 28// answered UnauthorizedException. 2.0.0 maps a bare name to `/channels/<name>`. 29// 3.0.0 is what the app uses, but the browser CDN 403s on it (2026-09-21). 30const SDK_URL = 'https://cdn.platform.uengage.io/browser-sdk/2.0.0/realtime.min.js'; 31// Update in lockstep with SDK_URL's version or the script is blocked outright. 32const SDK_INTEGRITY = 'sha384-VbzEY+ZQo51Y1Mizk5Wiu9jg7hqZkRXZMaRddaD6DSbG1X6VS9LM5Xe+RldpmuYi'; 33 34const API_BASE = (window.AppConfig?.apiBase || '').replace(/\/$/, ''); 35const ACTIVE_PATH = '/client/getActiveOrders'; 36 37let sdkPromise = null; 38 39function cfg() { 40 return window.AppConfig?.realtime || {}; 41} 42 43/** The brand opt-out flag, and nothing else â there is no per-environment config. */ 44export function isRealtimeConfigured() { 45 return !!cfg().enabled; 46} 47 48// The SDK cannot be handed a token: its three auth modes are tokenUrl, 49// client_credentials (a secret in the page: never) and none. But `fetchFn` is a 50// documented option and the tokenUrl mint goes through it, so we point it at a 51// URL that never leaves the page and answer that request ourselves. 52const TOKEN_SENTINEL = 'https://realtime.invalid/token'; 53 54/** 55 * cfg + this order's credential â SDK createClient() options. 56 * `cred` may be the credential object or a getter returning the current one. 57 */ 58export function buildClientOptions(c, cred) { 59 const opts = { transport: 'auto', tokenUrl: TOKEN_SENTINEL }; 60 if (c.env) opts.env = c.env; 61 62 const poll = parseInt(c.pollIntervalMs, 10); 63 if (!isNaN(poll) && poll > 0) opts.pollIntervalMs = poll; 64 65 // Answer the mint ourselves; delegate every other request to the real fetch. 66 // `expires_in` is SECONDS and backend reports REMAINING life, not the original 67 // duration â pass it straight through. 68 opts.fetchFn = function (input, init) { 69 const url = typeof input === 'string' ? input : (input && input.url); 70 if (url !== TOKEN_SENTINEL) return fetch(input, init); 71 // Resolved per call, never captured: `cred` is a getter into the caller's 72 // live credential, which a later cart read replaces wholesale. 73 const now = typeof cred === 'function' ? cred() : cred; 74 if (!now || !now.token) { 75 return Promise.resolve(new Response( 76 JSON.stringify({ error: 'no realtime credential for this order' }), 77 { status: 401, headers: { 'Content-Type': 'application/json' } } 78 )); 79 } 80 return Promise.resolve(new Response( 81 JSON.stringify({ token: now.token, expires_in: now.expiresIn }), 82 { status: 200, headers: { 'Content-Type': 'application/json' } } 83 )); 84 }; 85 86 return opts; 87} 88 89// ââ SDK loader ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 90// Same shape as payment.js's Razorpay loader: promise-cached, with a 91// querySelector guard against a duplicate <script>. 92 93function loadSdk() {
94 if (sdkPromise) return sdkPromise; 95 sdkPromise = new Promise((resolve, reject) => { 96 if (window.Uengage?.realtime) { resolve(); return; } 97 98 const existing = document.querySelector(`script[src="${SDK_URL}"]`); 99 if (existing) { 100 existing.addEventListener('load', () => resolve()); 101 existing.addEventListener('error', () => reject(new Error('Realtime SDK failed to load'))); 102 return; 103 } 104 105 const script = document.createElement('script'); 106 script.src = SDK_URL; 107 script.async = true; 108 script.integrity = SDK_INTEGRITY; 109 script.crossOrigin = 'anonymous'; 110 script.onload = () => resolve(); 111 script.onerror = () => { 112 // Clear the memo, or loadSdk()'s `if (sdkPromise) return sdkPromise` 113 // hands back this rejected promise forever and realtime stays dead for 114 // the session even once connectivity returns. 115 sdkPromise = null; 116 reject(new Error('Realtime SDK failed to load')); 117 }; 118 document.head.appendChild(script); 119 }); 120 return sdkPromise; 121} 122 123// ââ Active-order gate âââââââââââââââââââââââââââââââââââââââââââââââââ 124 125function getUser() { 126 for (const key of ['userdata', 'authState']) { 127 try { 128 const raw = localStorage.getItem(key); 129 if (raw && raw !== 'null') { 130 const obj = JSON.parse(raw); 131 if (obj && (obj.contactMappingId || obj.contact_mapping_id)) return obj; 132 } 133 } catch { /* corrupt entry â try the next key */ } 134 } 135 return null; 136} 137 138/** 139 * Is this order still live? Gating on it means a delivered order never opens a 140 * socket it would immediately close. Fails CLOSED: a wrong "yes" costs a stuck 141 * socket, a wrong "no" costs only latency we already tolerate. 142 * 143 * The fetch + localStorage read are duplicated from activeOrders.js deliberately 144 * â adding exports to an already-cached module has broken pages here before. 145 */ 146async function isOrderActive(orderId) { 147 const user = getUser(); 148 if (!user || !orderId) return false; 149 150 const params = new URLSearchParams({ 151 contactMappingId: String(user.contactMappingId ?? user.contact_mapping_id ?? ''), 152 token: String(user.token ?? ''), 153 // This endpoint wants `mobileNo`; userdata stores it as `mobile`. 154 mobileNo: String(user.mobile ?? user.mobileNo ?? ''), 155 // Parent id, never the outlet id â AppConfig.businessId IS the parent. 156 parentBusinessId: String(window.AppConfig?.businessId ?? ''), 157 }); 158 159 const res = await fetch(`${API_BASE}${ACTIVE_PATH}?${params}`, { 160 headers: { token: user.token || '', Authorization: 'Bearer ' + (user.token || '') }, 161 }); 162 if (!res.ok) return false; 163 164 const json = await res.json(); 165 const orders = Array.isArray(json?.active_orders) ? json.active_orders : []; 166 const wanted = String(orderId); 167 168 return orders.some((o) => String(o.orderId ?? o.order_id ?? o.id ?? '') === wanted); 169} 170 171// ââ Subscription ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 172 173/** 174 * Subscribe to one order's push updates. Returns unsubscribe SYNCHRONOUSLY even 175 * though the work behind it is async; calling it before that settles cancels the 176 * chain. onSignal(event) receives the raw platform event. 177 */ 178export function subscribeToOrder(orderId, onSignal, opts) { 179 if (!isRealtimeConfigured() || !orderId || typeof onSignal !== 'function') { 180 // Every bail logs its reason: a socket that never opened is otherwise 181 // indistinguishable from one that opened and received nothing. 182 console.debug('[realtime] not started â brand flag off or bad arguments'); 183 return function noop() {}; 184 } 185 186 let cancelled = false; 187 let teardown = null; 188 189 // Promise.resolve().then() so a synchronous throw inside createClient() lands 190 // in the same .catch() as an async rejection â one failure path, not tw
190o. 191 Promise.resolve() 192 // Prefer the caller's assertion: pastOrders.js already holds the order and 193 // has passed its own isTerminalOrder() check. It also dodges a real 194 // fragility â getActiveOrders filters on `mobileNo` while userdata stores 195 // `mobile`, so a stale one returns an empty list for a live order and would 196 // switch the SDK off for exactly the orders it exists to serve. 197 .then(() => (opts && typeof opts.active === 'boolean' ? opts.active : isOrderActive(orderId))) 198 .then((active) => { 199 if (!active || cancelled) { 200 if (!active) console.debug('[realtime] order is not active â no socket'); 201 return null; 202 } 203 return loadSdk(); 204 }) 205 .then((loaded) => { 206 if (loaded === null || cancelled) return; 207 208 const create = window.Uengage?.realtime?.createClient; 209 if (typeof create !== 'function') { 210 console.warn('[realtime] SDK loaded but createClient is missing'); 211 return; 212 } 213 214 // The order's own credential, off the client/cart response. Without it 215 // there is nothing to authorise with, and the poll carries the page. 216 // `credentials` may be a getter. It has to be, for the re-mint to work: 217 // the caller replaces its credential object on every poll, so a value 218 // captured here would pin fetchFn to the ORIGINAL expires_in â and since 219 // backend reports REMAINING life, that is the largest figure it will ever 220 // send. The SDK would schedule the re-mint off it, ask after the token had 221 // actually lapsed, take a 401, mark the error permanent and tear down. 222 const credSrc = opts && opts.credentials; 223 const credNow = typeof credSrc === 'function' ? credSrc() : credSrc; 224 if (!credNow || !credNow.token || !credNow.channel) { 225 console.warn('[realtime] no credential for this order â poll continues', 226 { token: !!(credNow && credNow.token), channel: credNow && credNow.channel }); 227 return; 228 } 229 console.debug('[realtime] subscribing to', credNow.channel); 230 231 // The channel is fixed for the order's life, so it is read once. Only the 232 // token needs to stay live, which is why the SOURCE goes to fetchFn. 233 const cred = credNow; 234 const client = create(buildClientOptions(cfg(), credSrc)); 235 // The server's channel string, VERBATIM â never constructed here. A 236 // mismatch is silent: valid token, healthy socket, no events, no error. 237 const sub = client.subscribe(cred.channel, { 238 onUpdate: (event) => { if (!cancelled) onSignal(event); }, 239 // `permanent` means retrying cannot help. Tear down so the SDK stops 240 // polling a route that will keep saying no. 241 onError: (err) => { 242 if (err?.permanent) { 243 console.warn('[realtime] disabled (' + (err.code || 'permanent') + '), poll continues', err); 244 if (teardown) { teardown(); teardown = null; } 245 return; 246 } 247 console.warn('[realtime] transient error, SDK retrying', err); 248 }, 249 }); 250 251 teardown = () => { 252 try { sub?.unsubscribe?.(); } catch { /* already gone */ } 253 try { client?.close?.(); } catch { /* already gone */ } 254 }; 255 256 // No refresh timer: the SDK re-mints through fetchFn before the token 257 // lapses, reading whichever credential the latest cart read left behind. 258 259 if (cancelled) teardown(); 260 }) 261 .catch((err) => console.warn('[realtime] unavailable, poll continues', err)); 262 263 return function unsubscribe() { 264 cancelled = true; 265 if (teardown) { teardown(); teardown = null; } 266 }; 267}
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.