@eshal-bot/chat-widget 0.1.47 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -99
- package/dist/chat-widget.esm.js +2 -2
- package/dist/chat-widget.esm.js.map +1 -0
- package/dist/chat-widget.js +283 -55
- package/dist/chat-widget.js.map +1 -0
- package/dist/chat-widget.min.js +2 -2
- package/dist/chat-widget.umd.js +2 -2
- package/dist/chat-widget.umd.js.map +1 -0
- package/package.json +2 -2
package/dist/chat-widget.js
CHANGED
|
@@ -8454,9 +8454,9 @@
|
|
|
8454
8454
|
}
|
|
8455
8455
|
};
|
|
8456
8456
|
|
|
8457
|
-
/**
|
|
8458
|
-
* Initializes the PCM audio player worklet that streams inline PCM chunks
|
|
8459
|
-
* coming from the ADK websocket events.
|
|
8457
|
+
/**
|
|
8458
|
+
* Initializes the PCM audio player worklet that streams inline PCM chunks
|
|
8459
|
+
* coming from the ADK websocket events.
|
|
8460
8460
|
*/
|
|
8461
8461
|
async function startAudioPlayerWorklet() {
|
|
8462
8462
|
if (typeof window === 'undefined') {
|
|
@@ -8480,9 +8480,9 @@
|
|
|
8480
8480
|
return [audioPlayerNode, audioContext];
|
|
8481
8481
|
}
|
|
8482
8482
|
|
|
8483
|
-
/**
|
|
8484
|
-
* Bootstraps the PCM recorder audio worklet, captures mic audio at 16kHz
|
|
8485
|
-
* mono, and forwards buffers through the provided handler.
|
|
8483
|
+
/**
|
|
8484
|
+
* Bootstraps the PCM recorder audio worklet, captures mic audio at 16kHz
|
|
8485
|
+
* mono, and forwards buffers through the provided handler.
|
|
8486
8486
|
*/
|
|
8487
8487
|
async function startAudioRecorderWorklet(onPcmChunk) {
|
|
8488
8488
|
if (typeof window === 'undefined') {
|
|
@@ -8519,8 +8519,8 @@
|
|
|
8519
8519
|
return [audioRecorderNode, audioRecorderContext, micStream];
|
|
8520
8520
|
}
|
|
8521
8521
|
|
|
8522
|
-
/**
|
|
8523
|
-
* Stops all microphone tracks.
|
|
8522
|
+
/**
|
|
8523
|
+
* Stops all microphone tracks.
|
|
8524
8524
|
*/
|
|
8525
8525
|
function stopMicrophone(stream) {
|
|
8526
8526
|
if (!stream) {
|
|
@@ -8537,24 +8537,24 @@
|
|
|
8537
8537
|
return pcm16Array.buffer;
|
|
8538
8538
|
}
|
|
8539
8539
|
|
|
8540
|
-
/**
|
|
8541
|
-
* Client-side Voice-Activity gate for the widget's voice (Bidi) session.
|
|
8542
|
-
*
|
|
8543
|
-
* Ported from packages/agent-sdk/src/voice/audio/vad.ts (`createSpeechGate`),
|
|
8544
|
-
* which was itself originally derived from this widget. The gate sits between
|
|
8545
|
-
* the mic worklet and the WebSocket: it forwards PCM ONLY inside a confirmed
|
|
8546
|
-
* speech turn and fires `onSpeechEnd` when the turn closes, so the caller can
|
|
8547
|
-
* tell the backend the utterance is done (an `audio_end` signal).
|
|
8548
|
-
*
|
|
8549
|
-
* Why the widget needs this: knowledge-api's non-streaming voice path only
|
|
8550
|
-
* transcribes a turn when it receives `audio_end` (or its flaky energy-VAD
|
|
8551
|
-
* backstop trips). Without a client gate the widget streamed mic audio forever
|
|
8552
|
-
* and the backend just buffered it ("Collecting Audio…") without ever
|
|
8553
|
-
* transcribing — so voice connected but never replied. The agent-sdk already
|
|
8554
|
-
* gates this way; this brings the widget to parity.
|
|
8555
|
-
*
|
|
8556
|
-
* Framework-free and side-effect-free (only calls the two callbacks) so it can
|
|
8557
|
-
* be reasoned about / tested in isolation.
|
|
8540
|
+
/**
|
|
8541
|
+
* Client-side Voice-Activity gate for the widget's voice (Bidi) session.
|
|
8542
|
+
*
|
|
8543
|
+
* Ported from packages/agent-sdk/src/voice/audio/vad.ts (`createSpeechGate`),
|
|
8544
|
+
* which was itself originally derived from this widget. The gate sits between
|
|
8545
|
+
* the mic worklet and the WebSocket: it forwards PCM ONLY inside a confirmed
|
|
8546
|
+
* speech turn and fires `onSpeechEnd` when the turn closes, so the caller can
|
|
8547
|
+
* tell the backend the utterance is done (an `audio_end` signal).
|
|
8548
|
+
*
|
|
8549
|
+
* Why the widget needs this: knowledge-api's non-streaming voice path only
|
|
8550
|
+
* transcribes a turn when it receives `audio_end` (or its flaky energy-VAD
|
|
8551
|
+
* backstop trips). Without a client gate the widget streamed mic audio forever
|
|
8552
|
+
* and the backend just buffered it ("Collecting Audio…") without ever
|
|
8553
|
+
* transcribing — so voice connected but never replied. The agent-sdk already
|
|
8554
|
+
* gates this way; this brings the widget to parity.
|
|
8555
|
+
*
|
|
8556
|
+
* Framework-free and side-effect-free (only calls the two callbacks) so it can
|
|
8557
|
+
* be reasoned about / tested in isolation.
|
|
8558
8558
|
*/
|
|
8559
8559
|
|
|
8560
8560
|
const DEFAULT_SPEECH_GATE_CONFIG = {
|
|
@@ -8583,16 +8583,16 @@
|
|
|
8583
8583
|
return Math.sqrt(sumSquares / samples.length);
|
|
8584
8584
|
}
|
|
8585
8585
|
|
|
8586
|
-
/**
|
|
8587
|
-
* Create a stateful speech gate. Returns `{ process, reset, isSpeaking }`.
|
|
8588
|
-
* `config.sampleRate` is required; everything else falls back to
|
|
8589
|
-
* DEFAULT_SPEECH_GATE_CONFIG.
|
|
8590
|
-
*
|
|
8591
|
-
* @param {{ sampleRate: number, energyThreshold?: number, speechStartMs?: number, hangoverMs?: number, preRollMs?: number }} config
|
|
8592
|
-
* @param {{ onSpeechStart?: () => void, onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8593
|
-
* onSpeechStart fires the instant a turn opens — use it for barge-in
|
|
8594
|
-
* (stop any assistant TTS still playing). onVoiceFrame emits each voiced
|
|
8595
|
-
* PCM frame; onSpeechEnd fires when the turn closes after the hangover.
|
|
8586
|
+
/**
|
|
8587
|
+
* Create a stateful speech gate. Returns `{ process, reset, isSpeaking }`.
|
|
8588
|
+
* `config.sampleRate` is required; everything else falls back to
|
|
8589
|
+
* DEFAULT_SPEECH_GATE_CONFIG.
|
|
8590
|
+
*
|
|
8591
|
+
* @param {{ sampleRate: number, energyThreshold?: number, speechStartMs?: number, hangoverMs?: number, preRollMs?: number }} config
|
|
8592
|
+
* @param {{ onSpeechStart?: () => void, onVoiceFrame: (pcm: ArrayBuffer) => void, onSpeechEnd: () => void }} callbacks
|
|
8593
|
+
* onSpeechStart fires the instant a turn opens — use it for barge-in
|
|
8594
|
+
* (stop any assistant TTS still playing). onVoiceFrame emits each voiced
|
|
8595
|
+
* PCM frame; onSpeechEnd fires when the turn closes after the hangover.
|
|
8596
8596
|
*/
|
|
8597
8597
|
function createSpeechGate(config, callbacks) {
|
|
8598
8598
|
var _config$energyThresho, _config$speechStartMs, _config$hangoverMs, _config$preRollMs;
|
|
@@ -9288,6 +9288,14 @@
|
|
|
9288
9288
|
} = await Promise.resolve().then(function () { return index; });
|
|
9289
9289
|
const tracer = new UserTrace({
|
|
9290
9290
|
cache: true,
|
|
9291
|
+
// Capture the visitor's precise location via the browser Geolocation
|
|
9292
|
+
// API (behind the native permission prompt) and reverse-geocode it
|
|
9293
|
+
// into profile.geo, so "Traffic by location" reflects the real city
|
|
9294
|
+
// instead of the coarse/wrong IP-gateway city. Granted → accurate
|
|
9295
|
+
// GPS; denied/dismissed/plain-HTTP → graceful IP fallback. Set
|
|
9296
|
+
// explicitly (not relying on the SDK default) so the behavior is
|
|
9297
|
+
// durable even if @eshal-tracker/sdk changes its default later.
|
|
9298
|
+
geoFromBrowser: true,
|
|
9291
9299
|
onError: () => {/* swallow — fingerprint is best-effort */}
|
|
9292
9300
|
});
|
|
9293
9301
|
const profile = await tracer.init();
|
|
@@ -59479,8 +59487,8 @@
|
|
|
59479
59487
|
});
|
|
59480
59488
|
};
|
|
59481
59489
|
|
|
59482
|
-
/**
|
|
59483
|
-
* Auto-scroll hook that scrolls to bottom when dependencies change
|
|
59490
|
+
/**
|
|
59491
|
+
* Auto-scroll hook that scrolls to bottom when dependencies change
|
|
59484
59492
|
*/
|
|
59485
59493
|
function useAutoScroll(containerRef) {
|
|
59486
59494
|
let dependencies = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
@@ -61939,6 +61947,16 @@
|
|
|
61939
61947
|
})
|
|
61940
61948
|
}];
|
|
61941
61949
|
|
|
61950
|
+
// Reorder the keyless cascade: freeipapi.com is currently the only one
|
|
61951
|
+
// reliably reachable from a browser — ip-api.com's free tier is HTTP-only
|
|
61952
|
+
// (→ HTTP 403 over HTTPS), ipwho.is → 403, ipapi.co → 429. Trying freeipapi
|
|
61953
|
+
// first avoids three doomed round-trips on every lookup. The rest stay as
|
|
61954
|
+
// fallback in case they recover or behave differently from another network.
|
|
61955
|
+
// NOTE: all IP providers locate the ISP gateway, not the device — for an
|
|
61956
|
+
// accurate end-user position use getBrowserLocation() (GPS + reverseGeocode).
|
|
61957
|
+
const FREE_ORDER = ["freeipapi.com", "ipwho.is", "ipapi.co", "ip-api.com"];
|
|
61958
|
+
freeProviders.sort((a, b) => FREE_ORDER.indexOf(a.name) - FREE_ORDER.indexOf(b.name));
|
|
61959
|
+
|
|
61942
61960
|
// Key-based providers go first (more reliable, higher limits)
|
|
61943
61961
|
return [...keyedProviders, ...freeProviders];
|
|
61944
61962
|
}
|
|
@@ -62036,22 +62054,130 @@
|
|
|
62036
62054
|
}));
|
|
62037
62055
|
}
|
|
62038
62056
|
|
|
62057
|
+
// Default options for navigator.geolocation.getCurrentPosition.
|
|
62058
|
+
// enableHighAccuracy → prefer real GPS over coarse network positioning;
|
|
62059
|
+
// timeout 8s → don't hang forever waiting for a fix;
|
|
62060
|
+
// maximumAge 5 min → accept a recently-cached fix so a silent refresh
|
|
62061
|
+
// (permission already granted) resolves instantly.
|
|
62062
|
+
const DEFAULT_GEO_OPTIONS = {
|
|
62063
|
+
enableHighAccuracy: true,
|
|
62064
|
+
timeout: 8000,
|
|
62065
|
+
maximumAge: 5 * 60 * 1000
|
|
62066
|
+
};
|
|
62067
|
+
|
|
62039
62068
|
/**
|
|
62040
|
-
* Get browser-native geolocation (GPS / network-based
|
|
62069
|
+
* Get browser-native geolocation (GPS / network-based).
|
|
62070
|
+
* Triggers the native permission prompt on first use for an origin (unless the
|
|
62071
|
+
* user already granted/denied). Resolves null — never rejects — on error,
|
|
62072
|
+
* denial, unsupported, or insecure (plain-HTTP) context, so callers can simply
|
|
62073
|
+
* fall back to IP geo.
|
|
62074
|
+
*
|
|
62075
|
+
* @param {PositionOptions} [options] - merged over DEFAULT_GEO_OPTIONS
|
|
62076
|
+
* @returns {Promise<{latitude:number, longitude:number, accuracy:number}|null>}
|
|
62041
62077
|
*/
|
|
62042
|
-
function getBrowserGeolocation$1(options = {
|
|
62043
|
-
timeout: 8000
|
|
62044
|
-
}) {
|
|
62078
|
+
function getBrowserGeolocation$1(options = {}) {
|
|
62045
62079
|
return new Promise(resolve => {
|
|
62046
|
-
if (!navigator
|
|
62080
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) return resolve(null);
|
|
62081
|
+
// getCurrentPosition only resolves in a secure context (HTTPS or
|
|
62082
|
+
// localhost). Bail early on plain HTTP rather than attempt a doomed prompt.
|
|
62083
|
+
if (typeof window !== "undefined" && window.isSecureContext === false) return resolve(null);
|
|
62047
62084
|
navigator.geolocation.getCurrentPosition(pos => resolve({
|
|
62048
62085
|
latitude: pos.coords.latitude,
|
|
62049
62086
|
longitude: pos.coords.longitude,
|
|
62050
62087
|
accuracy: pos.coords.accuracy
|
|
62051
|
-
}), () => resolve(null),
|
|
62088
|
+
}), () => resolve(null), {
|
|
62089
|
+
...DEFAULT_GEO_OPTIONS,
|
|
62090
|
+
...options
|
|
62091
|
+
});
|
|
62052
62092
|
});
|
|
62053
62093
|
}
|
|
62054
62094
|
|
|
62095
|
+
/**
|
|
62096
|
+
* Query the current geolocation permission WITHOUT triggering a prompt.
|
|
62097
|
+
* Lets the host decide whether to ask: "granted" → fetch silently, "denied" →
|
|
62098
|
+
* skip (IP fallback), "prompt" → show your own pre-ask before getCurrentPosition.
|
|
62099
|
+
* Returns "unsupported" where the Permissions API is unavailable (e.g. older
|
|
62100
|
+
* Safari) so callers can treat it as "not known to be granted".
|
|
62101
|
+
* @returns {Promise<"granted"|"denied"|"prompt"|"unsupported">}
|
|
62102
|
+
*/
|
|
62103
|
+
async function getGeolocationPermission$1() {
|
|
62104
|
+
try {
|
|
62105
|
+
if (typeof navigator === "undefined" || !navigator.permissions || !navigator.permissions.query) {
|
|
62106
|
+
return "unsupported";
|
|
62107
|
+
}
|
|
62108
|
+
const status = await navigator.permissions.query({
|
|
62109
|
+
name: "geolocation"
|
|
62110
|
+
});
|
|
62111
|
+
return status.state; // "granted" | "denied" | "prompt"
|
|
62112
|
+
} catch {
|
|
62113
|
+
return "unsupported";
|
|
62114
|
+
}
|
|
62115
|
+
}
|
|
62116
|
+
|
|
62117
|
+
/**
|
|
62118
|
+
* Reverse-geocode latitude/longitude → city / region / country.
|
|
62119
|
+
* Uses BigDataCloud's keyless, CORS-enabled *client* endpoint (no API key,
|
|
62120
|
+
* accurate to the city/state level). Resolves null on network failure or an
|
|
62121
|
+
* unusable response so the caller can keep any prior (IP-based) values.
|
|
62122
|
+
*
|
|
62123
|
+
* @param {number} latitude
|
|
62124
|
+
* @param {number} longitude
|
|
62125
|
+
* @param {Object} [options]
|
|
62126
|
+
* @param {number} [options.timeoutMs]
|
|
62127
|
+
* @param {string} [options.localityLanguage="en"]
|
|
62128
|
+
* @returns {Promise<{city,region,regionCode,country,countryCode,latitude,longitude}|null>}
|
|
62129
|
+
*/
|
|
62130
|
+
async function reverseGeocode$1(latitude, longitude, options = {}) {
|
|
62131
|
+
if (typeof latitude !== "number" || typeof longitude !== "number") return null;
|
|
62132
|
+
const timeoutMs = options.timeoutMs || _globalConfig.timeoutMs || 5000;
|
|
62133
|
+
const lang = options.localityLanguage || "en";
|
|
62134
|
+
const url = "https://api.bigdatacloud.net/data/reverse-geocode-client" + `?latitude=${latitude}&longitude=${longitude}&localityLanguage=${lang}`;
|
|
62135
|
+
try {
|
|
62136
|
+
const d = await fetchWithTimeout(url, timeoutMs);
|
|
62137
|
+
const city = d.city || d.locality || "";
|
|
62138
|
+
// Treat an empty body (e.g. mid-ocean coords) as a miss, not a result.
|
|
62139
|
+
if (!city && !d.countryName) return null;
|
|
62140
|
+
return {
|
|
62141
|
+
city,
|
|
62142
|
+
region: d.principalSubdivision || "",
|
|
62143
|
+
regionCode: (d.principalSubdivisionCode || "").split("-").pop() || "",
|
|
62144
|
+
country: d.countryName || "",
|
|
62145
|
+
countryCode: d.countryCode || "",
|
|
62146
|
+
latitude,
|
|
62147
|
+
longitude
|
|
62148
|
+
};
|
|
62149
|
+
} catch {
|
|
62150
|
+
return null;
|
|
62151
|
+
}
|
|
62152
|
+
}
|
|
62153
|
+
|
|
62154
|
+
/**
|
|
62155
|
+
* High-level "exact location" helper. Gets the device GPS fix (prompting if
|
|
62156
|
+
* needed) and reverse-geocodes it. Returns a geo-shaped object tagged
|
|
62157
|
+
* source:"gps" when the place resolved; the bare coords (no source tag) when
|
|
62158
|
+
* only the fix succeeded but reverse-geocoding failed; or null when there is no
|
|
62159
|
+
* fix at all (denied / unsupported / insecure). This is the accurate-location
|
|
62160
|
+
* primitive — prefer it over IP geo for the real end-user position.
|
|
62161
|
+
*
|
|
62162
|
+
* @param {PositionOptions} [options]
|
|
62163
|
+
* @returns {Promise<Object|null>}
|
|
62164
|
+
*/
|
|
62165
|
+
async function getBrowserLocation$1(options = {}) {
|
|
62166
|
+
const coords = await getBrowserGeolocation$1(options);
|
|
62167
|
+
if (!coords) return null;
|
|
62168
|
+
const place = await reverseGeocode$1(coords.latitude, coords.longitude, options);
|
|
62169
|
+
const hasPlace = !!(place && (place.city || place.country));
|
|
62170
|
+
return {
|
|
62171
|
+
...(hasPlace ? place : {}),
|
|
62172
|
+
latitude: coords.latitude,
|
|
62173
|
+
longitude: coords.longitude,
|
|
62174
|
+
accuracy: coords.accuracy,
|
|
62175
|
+
...(hasPlace ? {
|
|
62176
|
+
source: "gps"
|
|
62177
|
+
} : {})
|
|
62178
|
+
};
|
|
62179
|
+
}
|
|
62180
|
+
|
|
62055
62181
|
/** Build a Google Maps URL from coordinates */
|
|
62056
62182
|
function toMapsUrl$1(lat, lon) {
|
|
62057
62183
|
return `https://www.google.com/maps?q=${lat},${lon}`;
|
|
@@ -62061,6 +62187,9 @@
|
|
|
62061
62187
|
resolveLocation: resolveLocation$1,
|
|
62062
62188
|
listProviders: listProviders$1,
|
|
62063
62189
|
getBrowserGeolocation: getBrowserGeolocation$1,
|
|
62190
|
+
getGeolocationPermission: getGeolocationPermission$1,
|
|
62191
|
+
reverseGeocode: reverseGeocode$1,
|
|
62192
|
+
getBrowserLocation: getBrowserLocation$1,
|
|
62064
62193
|
toMapsUrl: toMapsUrl$1
|
|
62065
62194
|
};
|
|
62066
62195
|
|
|
@@ -62244,6 +62373,9 @@
|
|
|
62244
62373
|
resolveLocation,
|
|
62245
62374
|
listProviders,
|
|
62246
62375
|
getBrowserGeolocation,
|
|
62376
|
+
getGeolocationPermission,
|
|
62377
|
+
reverseGeocode,
|
|
62378
|
+
getBrowserLocation,
|
|
62247
62379
|
toMapsUrl
|
|
62248
62380
|
} = location;
|
|
62249
62381
|
const {
|
|
@@ -62276,7 +62408,13 @@
|
|
|
62276
62408
|
* @param {Object} [options]
|
|
62277
62409
|
* @param {Function} [options.onReady] - Callback when profile is ready
|
|
62278
62410
|
* @param {Function} [options.onError] - Callback on error
|
|
62279
|
-
* @param {boolean}
|
|
62411
|
+
* @param {boolean|"auto"} [options.geoFromBrowser=true] - Capture precise GPS
|
|
62412
|
+
* and reverse-geocode it into `geo`. DEFAULT true = request on init()
|
|
62413
|
+
* (shows the native permission prompt unless already granted/denied);
|
|
62414
|
+
* false = never (IP only); "auto" = use GPS only when permission was
|
|
62415
|
+
* ALREADY granted (silent, never prompts).
|
|
62416
|
+
* @param {Object} [options.geoApiKeys] - API keys for keyed geo providers
|
|
62417
|
+
* @param {PositionOptions} [options.geoOptions] - Override getCurrentPosition opts
|
|
62280
62418
|
* @param {boolean} [options.cache=true] - Use cached profile if < maxAgeMs old
|
|
62281
62419
|
* @param {number} [options.maxAgeMs=300000] - Max cache age (default 5 min)
|
|
62282
62420
|
* @param {boolean} [options.autoInit=false] - Auto-init on construction
|
|
@@ -62286,7 +62424,13 @@
|
|
|
62286
62424
|
this._options = {
|
|
62287
62425
|
onReady: options.onReady || null,
|
|
62288
62426
|
onError: options.onError || null,
|
|
62289
|
-
|
|
62427
|
+
// Default ON: capture precise GPS on init() behind the browser's native
|
|
62428
|
+
// permission prompt. Use `=== undefined` (not `|| `) so an explicit
|
|
62429
|
+
// `false` opt-out is honored; pass "auto" to use an already-granted
|
|
62430
|
+
// permission silently without ever prompting.
|
|
62431
|
+
geoFromBrowser: options.geoFromBrowser === undefined ? true : options.geoFromBrowser,
|
|
62432
|
+
geoApiKeys: options.geoApiKeys || null,
|
|
62433
|
+
geoOptions: options.geoOptions || null,
|
|
62290
62434
|
cache: options.cache !== false,
|
|
62291
62435
|
maxAgeMs: options.maxAgeMs || 5 * 60 * 1000,
|
|
62292
62436
|
autoInit: options.autoInit || false,
|
|
@@ -62336,15 +62480,46 @@
|
|
|
62336
62480
|
confidence
|
|
62337
62481
|
} = generateFingerprint();
|
|
62338
62482
|
|
|
62339
|
-
// 3. IP
|
|
62340
|
-
|
|
62341
|
-
|
|
62342
|
-
const
|
|
62343
|
-
|
|
62344
|
-
|
|
62345
|
-
|
|
62346
|
-
|
|
62347
|
-
|
|
62483
|
+
// 3. IP→geo chain and (optional) precise browser GPS run concurrently.
|
|
62484
|
+
// GPS is the slow path (permission prompt + satellite fix) so we never
|
|
62485
|
+
// block the IP lookup on it.
|
|
62486
|
+
const ipApiKeys = this._options.geoApiKeys ? {
|
|
62487
|
+
apiKeys: this._options.geoApiKeys
|
|
62488
|
+
} : {};
|
|
62489
|
+
const [ipBundle, gpsLoc] = await Promise.all([(async () => {
|
|
62490
|
+
const ipResult = await detectIP();
|
|
62491
|
+
const ipAddr = ipResult?.ip || "unknown";
|
|
62492
|
+
const ipGeo = ipAddr !== "unknown" ? await resolveLocation(ipAddr, ipApiKeys) : null;
|
|
62493
|
+
return {
|
|
62494
|
+
ip: ipAddr,
|
|
62495
|
+
ipVersion: ipResult ? getIPVersion(ipAddr) : "unknown",
|
|
62496
|
+
geo: ipGeo
|
|
62497
|
+
};
|
|
62498
|
+
})(), this._collectBrowserLocation()]);
|
|
62499
|
+
const {
|
|
62500
|
+
ip,
|
|
62501
|
+
ipVersion
|
|
62502
|
+
} = ipBundle;
|
|
62503
|
+
|
|
62504
|
+
// 4. Geo: start from the (coarse) IP location, then — when we have a
|
|
62505
|
+
// precise GPS fix — OVERRIDE city/region/country with the
|
|
62506
|
+
// reverse-geocoded values. IP geo only locates the ISP gateway, so a
|
|
62507
|
+
// real device fix always wins. Denied/unavailable → IP value stays.
|
|
62508
|
+
let geo = ipBundle.geo;
|
|
62509
|
+
let browserGeo = null;
|
|
62510
|
+
if (gpsLoc) {
|
|
62511
|
+
browserGeo = {
|
|
62512
|
+
latitude: gpsLoc.latitude,
|
|
62513
|
+
longitude: gpsLoc.longitude,
|
|
62514
|
+
accuracy: gpsLoc.accuracy
|
|
62515
|
+
};
|
|
62516
|
+
if (gpsLoc.source === "gps") {
|
|
62517
|
+
// gpsLoc carries city/region/regionCode/country/countryCode/lat/lng/source
|
|
62518
|
+
geo = {
|
|
62519
|
+
...(geo || {}),
|
|
62520
|
+
...gpsLoc
|
|
62521
|
+
};
|
|
62522
|
+
}
|
|
62348
62523
|
}
|
|
62349
62524
|
|
|
62350
62525
|
// 5. Browser context
|
|
@@ -62386,6 +62561,56 @@
|
|
|
62386
62561
|
}
|
|
62387
62562
|
}
|
|
62388
62563
|
|
|
62564
|
+
/**
|
|
62565
|
+
* Decide whether to capture browser GPS during init(), honoring geoFromBrowser:
|
|
62566
|
+
* false → null (skip)
|
|
62567
|
+
* "auto" → only when permission is ALREADY granted (silent, never prompts)
|
|
62568
|
+
* true → attempt (getCurrentPosition shows the prompt unless denied)
|
|
62569
|
+
* @private
|
|
62570
|
+
* @returns {Promise<Object|null>}
|
|
62571
|
+
*/
|
|
62572
|
+
async _collectBrowserLocation() {
|
|
62573
|
+
const mode = this._options.geoFromBrowser;
|
|
62574
|
+
if (!mode) return null;
|
|
62575
|
+
if (mode === "auto") {
|
|
62576
|
+
const perm = await getGeolocationPermission();
|
|
62577
|
+
if (perm !== "granted") return null; // never prompt during a silent init
|
|
62578
|
+
}
|
|
62579
|
+
return getBrowserLocation(this._options.geoOptions || {});
|
|
62580
|
+
}
|
|
62581
|
+
|
|
62582
|
+
/**
|
|
62583
|
+
* Capture precise browser location AFTER your UI has obtained user consent
|
|
62584
|
+
* (e.g. an in-widget "Share your location?" pre-ask). Triggers the native
|
|
62585
|
+
* permission prompt, reverse-geocodes the fix, merges it into the current
|
|
62586
|
+
* profile's `geo` (source:"gps"), re-persists, and fires onReady. No-ops
|
|
62587
|
+
* gracefully when denied/unavailable (keeps the IP fallback) or before init().
|
|
62588
|
+
* @param {PositionOptions} [options]
|
|
62589
|
+
* @returns {Promise<UserProfile|null>}
|
|
62590
|
+
*/
|
|
62591
|
+
async captureBrowserLocation(options = {}) {
|
|
62592
|
+
const loc = await getBrowserLocation({
|
|
62593
|
+
...(this._options.geoOptions || {}),
|
|
62594
|
+
...options
|
|
62595
|
+
});
|
|
62596
|
+
if (!loc || !this.profile) return this.profile; // denied/unavailable or not init'd yet
|
|
62597
|
+
this.profile.browserGeo = {
|
|
62598
|
+
latitude: loc.latitude,
|
|
62599
|
+
longitude: loc.longitude,
|
|
62600
|
+
accuracy: loc.accuracy
|
|
62601
|
+
};
|
|
62602
|
+
this.profile.mapsUrl = toMapsUrl(loc.latitude, loc.longitude);
|
|
62603
|
+
if (loc.source === "gps") {
|
|
62604
|
+
this.profile.geo = {
|
|
62605
|
+
...(this.profile.geo || {}),
|
|
62606
|
+
...loc
|
|
62607
|
+
};
|
|
62608
|
+
}
|
|
62609
|
+
saveSessionMeta(this.profile);
|
|
62610
|
+
if (this._options.onReady) this._options.onReady(this.profile);
|
|
62611
|
+
return this.profile;
|
|
62612
|
+
}
|
|
62613
|
+
|
|
62389
62614
|
/**
|
|
62390
62615
|
* Get the current profile (must call init() first)
|
|
62391
62616
|
* @returns {UserProfile|null}
|
|
@@ -62480,6 +62705,9 @@
|
|
|
62480
62705
|
src.exports.resolveLocation = resolveLocation;
|
|
62481
62706
|
src.exports.listProviders = listProviders;
|
|
62482
62707
|
src.exports.getBrowserGeolocation = getBrowserGeolocation;
|
|
62708
|
+
src.exports.getGeolocationPermission = getGeolocationPermission;
|
|
62709
|
+
src.exports.reverseGeocode = reverseGeocode;
|
|
62710
|
+
src.exports.getBrowserLocation = getBrowserLocation;
|
|
62483
62711
|
src.exports.getOrCreateUserID = getOrCreateUserID;
|
|
62484
62712
|
src.exports.getOrCreateSessionID = getOrCreateSessionID;
|
|
62485
62713
|
src.exports.clearSession = clearSession;
|