callix-dialer-widget 2.8.3 → 2.8.4

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/WIDGET-README.md CHANGED
@@ -160,7 +160,13 @@ aparecia "(--) ----- ----" na aba espelho). Click-to-call com o agente em pausa
160
160
  agente sai da pausa, espera o Callix liberá-lo (até 8 s) e disca; se não liberar, a tarefa
161
161
  volta à fila. No modo motor, um `dial` recebido em pausa faz o mesmo dentro do motor.
162
162
 
163
- Sem `engineUrl` nada muda: o SDK continua rodando no navegador como sempre.
163
+ Saída confirmada (2.8.4): o widget aguarda o motor e o back-end confirmarem o
164
+ encerramento antes de apagar a credencial local. Se a saída falhar, mostra o erro
165
+ e permite tentar novamente; se o Chromium cair, tenta concluir a limpeza pela API.
166
+ O modo SDK direto também encerra a sessão no back-end. Atualize o widget, o
167
+ back-end e o motor juntos para usar este fluxo.
168
+
169
+ Sem `engineUrl` o SDK continua rodando no navegador.
164
170
 
165
171
  ## 🔐 Login e usuário conhecido
166
172
 
@@ -43832,7 +43832,12 @@ class EngineClient {
43832
43832
  const ws = new Impl(engineWsUrl(this.options.url));
43833
43833
  this.ws = ws;
43834
43834
  ws.onopen = () => {
43835
+ var _a2, _b;
43835
43836
  if (this.ws !== ws) return;
43837
+ if (this.pendingLogoffToken !== void 0 && ((_b = (_a2 = this.options).sessionToken) == null ? void 0 : _b.call(_a2)) !== this.pendingLogoffToken) {
43838
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
43839
+ this.rejectWaiters(new EngineAccessError("session-changed", "A sessão mudou antes de confirmar a saída."));
43840
+ }
43836
43841
  this.reconnectDelay = RECONNECT_MIN_MS;
43837
43842
  this.setState({ wsState: "open" });
43838
43843
  const sessionToken = !this.session && this.options.sessionToken ? this.options.sessionToken() : null;
@@ -43889,7 +43894,8 @@ class EngineClient {
43889
43894
  ws.onclose = null;
43890
43895
  ws.close();
43891
43896
  }
43892
- for (const waiter of this.viewWaiters.splice(0)) clearTimeout(waiter.timer);
43897
+ this.pendingCommands = [];
43898
+ this.rejectWaiters(new Error("a conexão com o motor foi encerrada"));
43893
43899
  this.setState({ wsState: "closed", tabId: null, engaged: false });
43894
43900
  }
43895
43901
  /**
@@ -43951,6 +43957,10 @@ class EngineClient {
43951
43957
  break;
43952
43958
  }
43953
43959
  if (message.code === "session-changed") {
43960
+ if (this.pendingLogoffToken !== void 0) {
43961
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
43962
+ this.rejectWaiters(new EngineAccessError(message.code, message.message));
43963
+ }
43954
43964
  this.session = null;
43955
43965
  this.log("a sessão deste agente foi trocada por um login novo; reconectando com a credencial atual");
43956
43966
  break;
@@ -44014,6 +44024,10 @@ class EngineClient {
44014
44024
  * (é assim que um login novo substitui a sessão de outro navegador).
44015
44025
  */
44016
44026
  setSession(session) {
44027
+ if (this.pendingLogoffToken !== void 0) {
44028
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command.name !== "logoff");
44029
+ this.rejectWaiters(new EngineAccessError("session-changed", "Uma sessão nova substituiu o pedido de saída anterior."));
44030
+ }
44017
44031
  this.session = session;
44018
44032
  this.setState({ rejected: null, sessionLost: null });
44019
44033
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
@@ -44042,6 +44056,35 @@ class EngineClient {
44042
44056
  release() {
44043
44057
  this.send({ type: "release" });
44044
44058
  }
44059
+ /** Só confirma a saída quando o host terminou o encerramento do SDK e da sessão. */
44060
+ async logoff(timeoutMs = 4e4) {
44061
+ var _a2, _b;
44062
+ if (this.state.rejected) throw new EngineAccessError(this.state.rejected.code, this.state.rejected.message);
44063
+ this.session = null;
44064
+ this.pendingCommands = [];
44065
+ this.pendingLogoffToken = (_b = (_a2 = this.options).sessionToken) == null ? void 0 : _b.call(_a2);
44066
+ const command = { name: "logoff" };
44067
+ let acknowledged = false;
44068
+ const confirmation = this.waitForView(timeoutMs, (view) => {
44069
+ if (view.loggingOff) {
44070
+ acknowledged = true;
44071
+ return false;
44072
+ }
44073
+ return view.engine === "stopped" || view.engine === "crashed" || acknowledged && Boolean(view.logoffError);
44074
+ }, { fresh: true });
44075
+ this.command(command);
44076
+ try {
44077
+ const view = await confirmation;
44078
+ if (view.logoffError) throw new Error(view.logoffError);
44079
+ if (!acknowledged || view.engine !== "stopped") throw new Error("O motor parou sem confirmar o encerramento da sessão no Callix.");
44080
+ } catch (error2) {
44081
+ if (error2 instanceof EngineAccessError) throw error2;
44082
+ throw new Error(error2 instanceof Error && error2.message !== "o motor não respondeu" ? error2.message : "O motor não confirmou o encerramento da sessão.");
44083
+ } finally {
44084
+ this.pendingCommands = this.pendingCommands.filter((entry) => entry.command !== command);
44085
+ this.pendingLogoffToken = void 0;
44086
+ }
44087
+ }
44045
44088
  command(command) {
44046
44089
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.state.tabId) {
44047
44090
  this.send({ type: "command", command });
@@ -60362,6 +60405,27 @@ function AuthenticatedMiniDialer({
60362
60405
  )
60363
60406
  ] });
60364
60407
  }
60408
+ async function endUserSdkSession(apiUrl, token, timeoutMs = 2e4) {
60409
+ const controller = new AbortController();
60410
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
60411
+ try {
60412
+ const response = await fetch(apiUrl, {
60413
+ method: "DELETE",
60414
+ headers: { Authorization: `Bearer ${token}`, "x-session-token": token },
60415
+ credentials: "same-origin",
60416
+ signal: controller.signal
60417
+ });
60418
+ const data = await response.json().catch(() => null);
60419
+ if (!response.ok || (data == null ? void 0 : data.success) === false) {
60420
+ throw new Error((data == null ? void 0 : data.error) || `O Callix não confirmou o encerramento da sessão (${response.status}).`);
60421
+ }
60422
+ } catch (error2) {
60423
+ if (controller.signal.aborted) throw new Error("O Callix não respondeu ao pedido de encerramento da sessão.");
60424
+ throw error2;
60425
+ } finally {
60426
+ clearTimeout(timer);
60427
+ }
60428
+ }
60365
60429
  const PROBE_TIMEOUT_MS = 600;
60366
60430
  const TAKEOVER_GRACE_MS = 3e3;
60367
60431
  const TAKEOVER_JITTER_MS = 600;
@@ -60814,7 +60878,7 @@ function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask
60814
60878
  }, [enabled, pollIntervalMs, sessionToken]);
60815
60879
  return null;
60816
60880
  }
60817
- const WIDGET_VERSION = "2.8.3";
60881
+ const WIDGET_VERSION = "2.8.4";
60818
60882
  let currentSipWebSocket = null;
60819
60883
  function sendSipUnregisterIfPossible(ws) {
60820
60884
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -61179,6 +61243,8 @@ function DialerWrapper({ options, containerEl }) {
61179
61243
  const isLoadingRef = useRef(false);
61180
61244
  isLoadingRef.current = isLoading;
61181
61245
  const logoffPendingRef = useRef(false);
61246
+ const logoutInFlightRef = useRef(null);
61247
+ const logoutRetryRef = useRef(false);
61182
61248
  const handleLoginRef = useRef(async () => void 0);
61183
61249
  const handleLogoutRef = useRef(async () => void 0);
61184
61250
  const cancelTakeover = useCallback(() => {
@@ -61223,6 +61289,7 @@ function DialerWrapper({ options, containerEl }) {
61223
61289
  log: (message) => console.log("[Callix Dialer/motor]", message)
61224
61290
  });
61225
61291
  client2.onSessionLost((lost) => {
61292
+ if (logoffPendingRef.current) return;
61226
61293
  const notice = describeSessionDrop(lost.originator);
61227
61294
  setError(`${notice.title}: ${notice.detail}${notice.action ? ` ${notice.action}` : ""}`);
61228
61295
  setIsAuthenticated(false);
@@ -61396,9 +61463,8 @@ function DialerWrapper({ options, containerEl }) {
61396
61463
  }
61397
61464
  if (!view) return;
61398
61465
  setEngineWaiting(false);
61399
- if (view.engine === "stopped" || view.engine === "crashed") logoffPendingRef.current = false;
61400
61466
  const hasSession = (view.engine === "running" || view.engine === "starting") && !sessionLost;
61401
- if (hasSession && !logoffPendingRef.current && !isAuthenticatedRef.current && !isLoadingRef.current && !joining) {
61467
+ if (hasSession && !view.loggingOff && !view.logoffError && !logoffPendingRef.current && !isAuthenticatedRef.current && !isLoadingRef.current && !joining) {
61402
61468
  joining = true;
61403
61469
  console.log("[Callix Dialer] o motor já segura a sessão deste agente; entrando por aqui também");
61404
61470
  void joinEngine(user).finally(() => {
@@ -61567,6 +61633,11 @@ function DialerWrapper({ options, containerEl }) {
61567
61633
  }, [engineMode, tabRole, username]);
61568
61634
  const handleLogin = useCallback(async (user) => {
61569
61635
  var _a2, _b, _c, _d;
61636
+ if (logoutInFlightRef.current) return;
61637
+ if (logoutRetryRef.current) {
61638
+ await handleLogoutRef.current({ broadcast: false });
61639
+ if (logoutRetryRef.current) return;
61640
+ }
61570
61641
  setIsLoading(true);
61571
61642
  setError(null);
61572
61643
  setLoginStatus("Preparando login...");
@@ -61585,6 +61656,9 @@ function DialerWrapper({ options, containerEl }) {
61585
61656
  console.log("[Callix Dialer] o motor recusou a credencial desta aba; fazendo login novo que substitui a sessão de lá");
61586
61657
  }
61587
61658
  const domain = engineDomain();
61659
+ if ((view == null ? void 0 : view.loggingOff) || (view == null ? void 0 : view.logoffError) && (view.engine === "running" || view.engine === "starting")) {
61660
+ throw new Error(view.logoffError || "A sessão anterior ainda está sendo encerrada. Aguarde antes de entrar novamente.");
61661
+ }
61588
61662
  const engineHasSession = view !== null && (view.engine === "running" || view.engine === "starting") && !view.sessionLost;
61589
61663
  let token = null;
61590
61664
  if (engineHasSession) {
@@ -61800,54 +61874,116 @@ function DialerWrapper({ options, containerEl }) {
61800
61874
  useEffect(() => {
61801
61875
  handleLoginRef.current = handleLogin;
61802
61876
  }, [handleLogin]);
61803
- const handleLogout = useCallback(async (opts) => {
61804
- var _a2, _b;
61877
+ const handleLogout = useCallback((opts) => {
61878
+ if (logoutInFlightRef.current) return logoutInFlightRef.current;
61805
61879
  const broadcast = (opts == null ? void 0 : opts.broadcast) !== false;
61806
61880
  const keepDecision = (opts == null ? void 0 : opts.keepDecision) === true;
61807
61881
  const currentUsername = username;
61882
+ const currentToken = sessionToken ?? (currentUsername ? readStoredToken(currentUsername) : null);
61883
+ const ownsSdkSession = tabRoleRef.current === "leader" || isAuthenticatedRef.current;
61808
61884
  cancelTakeover();
61809
- if (options.engineUrl) {
61810
- if (isAuthenticatedRef.current) {
61811
- logoffPendingRef.current = true;
61812
- (_a2 = engineRef.current) == null ? void 0 : _a2.command({ name: "logoff" });
61813
- }
61814
- } else {
61815
- if (broadcast) (_b = busRef.current) == null ? void 0 : _b.post({ type: "logout" });
61816
- if (cleanupRef.current) {
61817
- console.log("[Callix Dialer] 📴 Logout: Chamando goOffline()...");
61818
- cleanupRef.current.goOffline();
61819
- await new Promise((resolve) => setTimeout(resolve, 500));
61820
- console.log("[Callix Dialer] 🛑 Logout: Chamando stop()...");
61821
- cleanupRef.current.stop();
61822
- await new Promise((resolve) => setTimeout(resolve, 500));
61823
- }
61824
- if (currentUsername) releaseLeader(currentUsername, tabIdRef.current);
61825
- }
61826
- setIsAuthenticated(false);
61827
- setSessionToken(null);
61828
- setCallixDomain(null);
61885
+ setIsLoading(true);
61886
+ isLoadingRef.current = true;
61887
+ setLoginStatus("Encerrando sessão...");
61829
61888
  setError(null);
61830
- if (keepDecision) {
61831
- setTabRole("member");
61832
- if (currentUsername) saveTabRole(currentUsername, "member");
61833
- return;
61834
- }
61835
- setTabRole(null);
61836
- clearTabRole();
61837
- setUsername(presetUsername ?? null);
61838
- if (typeof window !== "undefined" && currentUsername) {
61889
+ const run = async () => {
61890
+ var _a2, _b;
61839
61891
  try {
61840
- const STORAGE_KEYS = getStorageKeys(currentUsername);
61841
- localStorage.removeItem(LAST_USERNAME_KEY);
61842
- localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
61843
- localStorage.removeItem(STORAGE_KEYS.DOMAIN);
61844
- localStorage.removeItem(STORAGE_KEYS.TIMESTAMP);
61845
- console.log("[Callix Dialer] Sessão removida do localStorage");
61892
+ if (options.engineUrl) {
61893
+ const client2 = engineRef.current;
61894
+ const state = client2 == null ? void 0 : client2.getState();
61895
+ if (!client2) throw new Error("Não há conexão com o motor para confirmar a saída.");
61896
+ if (!(state == null ? void 0 : state.rejected) && !(state == null ? void 0 : state.sessionLost)) {
61897
+ logoffPendingRef.current = true;
61898
+ const finishStoppedSession = async () => {
61899
+ if (!currentToken) throw new Error("O motor parou sem confirmar a saída e esta aba não tem o token da sessão.");
61900
+ const stored = currentUsername ? readStoredToken(currentUsername) : null;
61901
+ if (stored && stored !== currentToken) throw new EngineAccessError("session-changed", "Outra aba substituiu a sessão durante o encerramento.");
61902
+ await endUserSdkSession(options.apiUrl || "/api/user-sdk-session", currentToken);
61903
+ };
61904
+ const isStopped = () => {
61905
+ const view = client2.getState().view;
61906
+ return (view == null ? void 0 : view.engine) === "stopped" || (view == null ? void 0 : view.engine) === "crashed";
61907
+ };
61908
+ if (isStopped()) {
61909
+ await finishStoppedSession();
61910
+ } else {
61911
+ try {
61912
+ await client2.logoff();
61913
+ } catch (err) {
61914
+ if (!isStopped()) throw err;
61915
+ await finishStoppedSession();
61916
+ }
61917
+ }
61918
+ }
61919
+ } else {
61920
+ if (broadcast) (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "logout" });
61921
+ if (cleanupRef.current) {
61922
+ cleanupRef.current.goOffline();
61923
+ await new Promise((resolve) => setTimeout(resolve, 500));
61924
+ (_b = cleanupRef.current) == null ? void 0 : _b.stop();
61925
+ await new Promise((resolve) => setTimeout(resolve, 500));
61926
+ }
61927
+ if (ownsSdkSession && !keepDecision) {
61928
+ if (!currentToken) throw new Error("Não há token para encerrar a sessão no Callix.");
61929
+ const stored = currentUsername ? readStoredToken(currentUsername) : null;
61930
+ if (stored && stored !== currentToken) throw new EngineAccessError("session-changed", "Outra aba substituiu a sessão durante o encerramento.");
61931
+ await endUserSdkSession(options.apiUrl || "/api/user-sdk-session", currentToken);
61932
+ }
61933
+ if (currentUsername) releaseLeader(currentUsername, tabIdRef.current);
61934
+ }
61935
+ logoutRetryRef.current = false;
61936
+ logoffPendingRef.current = false;
61937
+ setIsAuthenticated(false);
61938
+ setSessionToken(null);
61939
+ currentSessionRef.current.token = null;
61940
+ setCallixDomain(null);
61941
+ if (keepDecision) {
61942
+ setTabRole("member");
61943
+ if (currentUsername) saveTabRole(currentUsername, "member");
61944
+ return;
61945
+ }
61946
+ setTabRole(null);
61947
+ clearTabRole();
61948
+ setUsername(presetUsername ?? null);
61949
+ if (typeof window !== "undefined" && currentUsername && (options.engineUrl || ownsSdkSession)) {
61950
+ try {
61951
+ const STORAGE_KEYS = getStorageKeys(currentUsername);
61952
+ if (localStorage.getItem(STORAGE_KEYS.SESSION_TOKEN) === currentToken) {
61953
+ if (localStorage.getItem(LAST_USERNAME_KEY) === currentUsername) localStorage.removeItem(LAST_USERNAME_KEY);
61954
+ localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
61955
+ localStorage.removeItem(STORAGE_KEYS.DOMAIN);
61956
+ localStorage.removeItem(STORAGE_KEYS.TIMESTAMP);
61957
+ }
61958
+ } catch (err) {
61959
+ console.warn("[Callix Dialer] Não foi possível limpar os dados locais da sessão:", err);
61960
+ }
61961
+ }
61846
61962
  } catch (err) {
61847
- console.warn("[Callix Dialer] Não foi possível limpar sessão:", err);
61963
+ console.error("[Callix Dialer] Não foi possível confirmar a saída:", err);
61964
+ if (err instanceof EngineAccessError && err.code === "session-changed") {
61965
+ logoutRetryRef.current = false;
61966
+ logoffPendingRef.current = false;
61967
+ setSessionToken(null);
61968
+ setIsAuthenticated(false);
61969
+ setError("A sessão foi substituída por outro login durante a saída. Entre novamente para usar a sessão atual.");
61970
+ return;
61971
+ }
61972
+ logoutRetryRef.current = true;
61973
+ setIsAuthenticated(false);
61974
+ setError(`Não foi possível confirmar a saída: ${err instanceof Error ? err.message : "falha ao encerrar a sessão"}. Ao entrar novamente, o discador tentará concluir o encerramento primeiro.`);
61975
+ } finally {
61976
+ setIsLoading(false);
61977
+ isLoadingRef.current = false;
61978
+ setLoginStatus(null);
61848
61979
  }
61849
- }
61850
- }, [username, presetUsername, options.engineUrl, cancelTakeover]);
61980
+ };
61981
+ const completion = run().finally(() => {
61982
+ logoutInFlightRef.current = null;
61983
+ });
61984
+ logoutInFlightRef.current = completion;
61985
+ return completion;
61986
+ }, [username, sessionToken, presetUsername, options.apiUrl, options.engineUrl, cancelTakeover]);
61851
61987
  useEffect(() => {
61852
61988
  handleLogoutRef.current = handleLogout;
61853
61989
  }, [handleLogout]);
@@ -61875,6 +62011,7 @@ function DialerWrapper({ options, containerEl }) {
61875
62011
  const handleSdkSessionDropped = useCallback(
61876
62012
  (originator, notice) => {
61877
62013
  var _a2;
62014
+ if (logoutInFlightRef.current) return;
61878
62015
  const user = username;
61879
62016
  if (user) releaseLeader(user, tabIdRef.current);
61880
62017
  (_a2 = busRef.current) == null ? void 0 : _a2.post({ type: "dropped", originator });