@eshal-bot/chat-widget 0.1.46 → 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 +299 -56
- 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] : [];
|
|
@@ -60081,6 +60089,21 @@
|
|
|
60081
60089
|
// Handle X button: first click shows form, second click closes
|
|
60082
60090
|
const handleCloseClick = reactExports.useCallback(() => {
|
|
60083
60091
|
if (!isCsatEnabled) {
|
|
60092
|
+
// CSAT disabled: closing the chat should also clear out the
|
|
60093
|
+
// conversation, mirroring the post-CSAT clear in
|
|
60094
|
+
// handleResolveSubmission. Without this the X button only hides
|
|
60095
|
+
// the window while the old transcript persists in the session and
|
|
60096
|
+
// re-appears on reopen / reload. Route through onRefreshChat (same
|
|
60097
|
+
// entry point the kebab "Refresh Chat" uses) so any change to the
|
|
60098
|
+
// refresh path applies here too, and clear widgetFilter explicitly.
|
|
60099
|
+
if (onWidgetFilterChange) {
|
|
60100
|
+
onWidgetFilterChange(null);
|
|
60101
|
+
}
|
|
60102
|
+
if (onRefreshChat) {
|
|
60103
|
+
onRefreshChat();
|
|
60104
|
+
} else if (onResetConversation) {
|
|
60105
|
+
onResetConversation();
|
|
60106
|
+
}
|
|
60084
60107
|
onClose === null || onClose === void 0 || onClose();
|
|
60085
60108
|
return;
|
|
60086
60109
|
}
|
|
@@ -60092,7 +60115,7 @@
|
|
|
60092
60115
|
} else {
|
|
60093
60116
|
onClose === null || onClose === void 0 || onClose();
|
|
60094
60117
|
}
|
|
60095
|
-
}, [isCsatEnabled, showResolveForm, isConversationResolved, onClose]);
|
|
60118
|
+
}, [isCsatEnabled, showResolveForm, isConversationResolved, onClose, onWidgetFilterChange, onRefreshChat, onResetConversation]);
|
|
60096
60119
|
reactExports.useEffect(() => {
|
|
60097
60120
|
if (isCsatEnabled) return;
|
|
60098
60121
|
if (showResolveForm) setShowResolveForm(false);
|
|
@@ -61924,6 +61947,16 @@
|
|
|
61924
61947
|
})
|
|
61925
61948
|
}];
|
|
61926
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
|
+
|
|
61927
61960
|
// Key-based providers go first (more reliable, higher limits)
|
|
61928
61961
|
return [...keyedProviders, ...freeProviders];
|
|
61929
61962
|
}
|
|
@@ -62021,22 +62054,130 @@
|
|
|
62021
62054
|
}));
|
|
62022
62055
|
}
|
|
62023
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
|
+
|
|
62024
62068
|
/**
|
|
62025
|
-
* 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>}
|
|
62026
62077
|
*/
|
|
62027
|
-
function getBrowserGeolocation$1(options = {
|
|
62028
|
-
timeout: 8000
|
|
62029
|
-
}) {
|
|
62078
|
+
function getBrowserGeolocation$1(options = {}) {
|
|
62030
62079
|
return new Promise(resolve => {
|
|
62031
|
-
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);
|
|
62032
62084
|
navigator.geolocation.getCurrentPosition(pos => resolve({
|
|
62033
62085
|
latitude: pos.coords.latitude,
|
|
62034
62086
|
longitude: pos.coords.longitude,
|
|
62035
62087
|
accuracy: pos.coords.accuracy
|
|
62036
|
-
}), () => resolve(null),
|
|
62088
|
+
}), () => resolve(null), {
|
|
62089
|
+
...DEFAULT_GEO_OPTIONS,
|
|
62090
|
+
...options
|
|
62091
|
+
});
|
|
62037
62092
|
});
|
|
62038
62093
|
}
|
|
62039
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
|
+
|
|
62040
62181
|
/** Build a Google Maps URL from coordinates */
|
|
62041
62182
|
function toMapsUrl$1(lat, lon) {
|
|
62042
62183
|
return `https://www.google.com/maps?q=${lat},${lon}`;
|
|
@@ -62046,6 +62187,9 @@
|
|
|
62046
62187
|
resolveLocation: resolveLocation$1,
|
|
62047
62188
|
listProviders: listProviders$1,
|
|
62048
62189
|
getBrowserGeolocation: getBrowserGeolocation$1,
|
|
62190
|
+
getGeolocationPermission: getGeolocationPermission$1,
|
|
62191
|
+
reverseGeocode: reverseGeocode$1,
|
|
62192
|
+
getBrowserLocation: getBrowserLocation$1,
|
|
62049
62193
|
toMapsUrl: toMapsUrl$1
|
|
62050
62194
|
};
|
|
62051
62195
|
|
|
@@ -62229,6 +62373,9 @@
|
|
|
62229
62373
|
resolveLocation,
|
|
62230
62374
|
listProviders,
|
|
62231
62375
|
getBrowserGeolocation,
|
|
62376
|
+
getGeolocationPermission,
|
|
62377
|
+
reverseGeocode,
|
|
62378
|
+
getBrowserLocation,
|
|
62232
62379
|
toMapsUrl
|
|
62233
62380
|
} = location;
|
|
62234
62381
|
const {
|
|
@@ -62261,7 +62408,13 @@
|
|
|
62261
62408
|
* @param {Object} [options]
|
|
62262
62409
|
* @param {Function} [options.onReady] - Callback when profile is ready
|
|
62263
62410
|
* @param {Function} [options.onError] - Callback on error
|
|
62264
|
-
* @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
|
|
62265
62418
|
* @param {boolean} [options.cache=true] - Use cached profile if < maxAgeMs old
|
|
62266
62419
|
* @param {number} [options.maxAgeMs=300000] - Max cache age (default 5 min)
|
|
62267
62420
|
* @param {boolean} [options.autoInit=false] - Auto-init on construction
|
|
@@ -62271,7 +62424,13 @@
|
|
|
62271
62424
|
this._options = {
|
|
62272
62425
|
onReady: options.onReady || null,
|
|
62273
62426
|
onError: options.onError || null,
|
|
62274
|
-
|
|
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,
|
|
62275
62434
|
cache: options.cache !== false,
|
|
62276
62435
|
maxAgeMs: options.maxAgeMs || 5 * 60 * 1000,
|
|
62277
62436
|
autoInit: options.autoInit || false,
|
|
@@ -62321,15 +62480,46 @@
|
|
|
62321
62480
|
confidence
|
|
62322
62481
|
} = generateFingerprint();
|
|
62323
62482
|
|
|
62324
|
-
// 3. IP
|
|
62325
|
-
|
|
62326
|
-
|
|
62327
|
-
const
|
|
62328
|
-
|
|
62329
|
-
|
|
62330
|
-
|
|
62331
|
-
|
|
62332
|
-
|
|
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
|
+
}
|
|
62333
62523
|
}
|
|
62334
62524
|
|
|
62335
62525
|
// 5. Browser context
|
|
@@ -62371,6 +62561,56 @@
|
|
|
62371
62561
|
}
|
|
62372
62562
|
}
|
|
62373
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
|
+
|
|
62374
62614
|
/**
|
|
62375
62615
|
* Get the current profile (must call init() first)
|
|
62376
62616
|
* @returns {UserProfile|null}
|
|
@@ -62465,6 +62705,9 @@
|
|
|
62465
62705
|
src.exports.resolveLocation = resolveLocation;
|
|
62466
62706
|
src.exports.listProviders = listProviders;
|
|
62467
62707
|
src.exports.getBrowserGeolocation = getBrowserGeolocation;
|
|
62708
|
+
src.exports.getGeolocationPermission = getGeolocationPermission;
|
|
62709
|
+
src.exports.reverseGeocode = reverseGeocode;
|
|
62710
|
+
src.exports.getBrowserLocation = getBrowserLocation;
|
|
62468
62711
|
src.exports.getOrCreateUserID = getOrCreateUserID;
|
|
62469
62712
|
src.exports.getOrCreateSessionID = getOrCreateSessionID;
|
|
62470
62713
|
src.exports.clearSession = clearSession;
|