callix-dialer-widget 2.7.0 → 2.8.0

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
@@ -119,18 +119,27 @@ CallixDialer.init({
119
119
  apiUrl: 'https://callix-zeta.vercel.app/api/user-sdk-session', // continua: é onde o login é feito
120
120
  uiVariant: 'mobile',
121
121
  engineUrl: 'https://motor.exemplo.com', // host do motor (HTTPS; o navegador exige origem segura para o microfone)
122
- engineToken: '...', // ENGINE_TAB_TOKEN do host
123
122
  });
124
123
  ```
125
124
 
126
- Também pelo HTML: `data-callix-engine-url` e `data-callix-engine-token` no container.
125
+ Também pelo HTML: `data-callix-engine-url` no container. Só isso: desde a 2.8.0 o widget não
126
+ precisa do token do host (`engineToken`). A credencial da aba é a própria sessão do Callix que
127
+ o widget cria em `apiUrl` e guarda no navegador: uma aba nova ou um F5 no mesmo navegador
128
+ apresenta esse token e entra na sessão que o motor roda; outro navegador ou computador não o
129
+ tem, vê "Entrar" com o aviso de que o discador está em uso em outro lugar, e ao entrar faz um
130
+ login novo que substitui a sessão de lá (como o Callix web faz). O host confirma no Callix
131
+ toda sessão entregue antes de subir o motor com ela. `engineToken` continua aceito, mas só
132
+ serve para uso administrativo (página `tab/` do motor, roteiros); não o coloque em página
133
+ pública.
127
134
 
128
135
  Fluxo de login no modo motor: o widget conecta ao host com o usuário; se o motor já segura
129
- a sessão desse agente, entra direto. Senão, faz o login em `apiUrl` e entrega a sessão ao
130
- motor, que sobe o SDK no servidor. O widget nunca cria uma sessão por cima de uma viva: o
131
- login exclusivo do Callix derrubaria a que o motor está usando. Se o Callix derrubar a
132
- sessão (login no Callix web, supervisor, bloqueio), o widget volta à tela de login com o
133
- motivo e não reentra sozinho.
136
+ a sessão desse agente e a aba tem a credencial dela, entra direto. Senão, faz o login em
137
+ `apiUrl` e entrega a sessão ao motor, que sobe o SDK no servidor. No mesmo navegador o
138
+ widget nunca cria uma sessão por cima de uma viva (o login exclusivo do Callix derrubaria a
139
+ que o motor está usando); de outro navegador ele só cria depois que o agente clica em
140
+ Entrar, avisado de que encerra a sessão de lá. Se o Callix derrubar a sessão (login no
141
+ Callix web, supervisor, bloqueio), o widget volta à tela de login com o motivo e não reentra
142
+ sozinho.
134
143
 
135
144
  Sem `engineUrl` nada muda: o SDK continua rodando no navegador como sempre.
136
145
 
@@ -43725,6 +43725,13 @@ function requireClientSdkReact() {
43725
43725
  return clientSdkReact;
43726
43726
  }
43727
43727
  var clientSdkReactExports = requireClientSdkReact();
43728
+ class EngineAccessError extends Error {
43729
+ constructor(code, message) {
43730
+ super(message);
43731
+ this.code = code;
43732
+ this.name = "EngineAccessError";
43733
+ }
43734
+ }
43728
43735
  class EngineSignal {
43729
43736
  constructor(value) {
43730
43737
  this.value = value;
@@ -43791,7 +43798,8 @@ class EngineClient {
43791
43798
  outputMuted: false,
43792
43799
  outputVolume: 100,
43793
43800
  transcript: INITIAL_TRANSCRIPT,
43794
- sessionLost: null
43801
+ sessionLost: null,
43802
+ rejected: null
43795
43803
  };
43796
43804
  }
43797
43805
  // ------------------------------------------------------------------ store
@@ -43824,7 +43832,16 @@ class EngineClient {
43824
43832
  if (this.ws !== ws) return;
43825
43833
  this.reconnectDelay = RECONNECT_MIN_MS;
43826
43834
  this.setState({ wsState: "open" });
43827
- const hello = { type: "hello", role: "tab", agentId: this.agentId, token: this.options.token, label: this.options.label ?? "Discador", ...this.session ? { session: this.session } : {} };
43835
+ const sessionToken = !this.session && this.options.sessionToken ? this.options.sessionToken() : null;
43836
+ const hello = {
43837
+ type: "hello",
43838
+ role: "tab",
43839
+ agentId: this.agentId,
43840
+ ...this.options.token ? { token: this.options.token } : {},
43841
+ ...sessionToken ? { sessionToken } : {},
43842
+ label: this.options.label ?? "Discador",
43843
+ ...this.session ? { session: this.session } : {}
43844
+ };
43828
43845
  this.send(hello);
43829
43846
  this.log("conectado ao host do motor");
43830
43847
  };
@@ -43871,18 +43888,30 @@ class EngineClient {
43871
43888
  for (const waiter of this.viewWaiters.splice(0)) clearTimeout(waiter.timer);
43872
43889
  this.setState({ wsState: "closed", tabId: null, engaged: false });
43873
43890
  }
43874
- /** a view atual, ou a próxima que o host mandar (welcome/view) */
43875
- waitForView(timeoutMs = 15e3) {
43876
- if (this.state.view && this.state.tabId) return Promise.resolve(this.state.view);
43891
+ /**
43892
+ * A view atual, ou a próxima que o host mandar (welcome/view). Com `accept`, espera uma
43893
+ * view que passe no filtro (ex.: motor subindo depois de entregar a sessão). Rejeita com
43894
+ * `EngineAccessError` se o host recusou esta aba (`rejected`).
43895
+ */
43896
+ waitForView(timeoutMs = 15e3, accept, options) {
43897
+ const rejected = this.state.rejected;
43898
+ if (rejected) return Promise.reject(new EngineAccessError(rejected.code, rejected.message));
43899
+ if (!(options == null ? void 0 : options.fresh) && this.state.view && this.state.tabId && (!accept || accept(this.state.view))) return Promise.resolve(this.state.view);
43877
43900
  return new Promise((resolve, reject) => {
43878
43901
  const timer = setTimeout(() => {
43879
43902
  const index = this.viewWaiters.findIndex((w) => w.timer === timer);
43880
43903
  if (index >= 0) this.viewWaiters.splice(index, 1);
43881
43904
  reject(new Error("o motor não respondeu"));
43882
43905
  }, timeoutMs);
43883
- this.viewWaiters.push({ resolve, timer });
43906
+ this.viewWaiters.push({ resolve, reject, accept, timer });
43884
43907
  });
43885
43908
  }
43909
+ rejectWaiters(error2) {
43910
+ for (const waiter of this.viewWaiters.splice(0)) {
43911
+ clearTimeout(waiter.timer);
43912
+ waiter.reject(error2);
43913
+ }
43914
+ }
43886
43915
  send(message) {
43887
43916
  if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
43888
43917
  }
@@ -43890,7 +43919,7 @@ class EngineClient {
43890
43919
  switch (message.type) {
43891
43920
  case "welcome": {
43892
43921
  if (Array.isArray(message.iceServers) && message.iceServers.length > 0) this.iceServers = message.iceServers;
43893
- this.setState({ tabId: message.tabId });
43922
+ this.setState({ tabId: message.tabId, rejected: null });
43894
43923
  this.applyView(message.view);
43895
43924
  this.log(`sou a aba ${message.tabId}`);
43896
43925
  if (this.options.autoEngage !== false && message.view.engagedTabId !== message.tabId) this.engage();
@@ -43908,6 +43937,19 @@ class EngineClient {
43908
43937
  void this.onSignal(message.payload);
43909
43938
  break;
43910
43939
  case "error":
43940
+ if (message.code === "unauthorized") {
43941
+ this.setState({ lastError: message.message, rejected: { code: message.code, message: message.message } });
43942
+ this.session = null;
43943
+ this.reconnectDelay = RECONNECT_MAX_MS;
43944
+ this.log(`acesso recusado pelo host: ${message.message}`);
43945
+ this.rejectWaiters(new EngineAccessError(message.code, message.message));
43946
+ break;
43947
+ }
43948
+ if (message.code === "session-changed") {
43949
+ this.session = null;
43950
+ this.log("a sessão deste agente foi trocada por um login novo; reconectando com a credencial atual");
43951
+ break;
43952
+ }
43911
43953
  this.setState({ lastError: message.message });
43912
43954
  this.log(`erro do host: ${message.message}`);
43913
43955
  break;
@@ -43945,6 +43987,10 @@ class EngineClient {
43945
43987
  if (call && typeof call.muted === "boolean" && call.muted !== this.state.inputMuted && call.state !== "ended") patch.inputMuted = call.muted;
43946
43988
  this.setState(patch);
43947
43989
  for (const waiter of this.viewWaiters.splice(0)) {
43990
+ if (waiter.accept && !waiter.accept(view)) {
43991
+ this.viewWaiters.push(waiter);
43992
+ continue;
43993
+ }
43948
43994
  clearTimeout(waiter.timer);
43949
43995
  waiter.resolve(view);
43950
43996
  }
@@ -43957,10 +44003,24 @@ class EngineClient {
43957
44003
  };
43958
44004
  }
43959
44005
  // ------------------------------------------------------------------ sessão e comandos
43960
- /** modo sdk: o widget fez login no back-end e entrega a sessão ao host */
44006
+ /**
44007
+ * Modo sdk: o widget fez login no back-end e entrega a sessão ao host. Com a conexão
44008
+ * aberta manda `session`; recusada ou caída, reconecta agora apresentando a sessão no hello
44009
+ * (é assim que um login novo substitui a sessão de outro navegador).
44010
+ */
43961
44011
  setSession(session) {
43962
44012
  this.session = session;
43963
- this.send({ type: "session", session });
44013
+ this.setState({ rejected: null, sessionLost: null });
44014
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
44015
+ this.send({ type: "session", session });
44016
+ return;
44017
+ }
44018
+ if (this.reconnectTimer) {
44019
+ clearTimeout(this.reconnectTimer);
44020
+ this.reconnectTimer = null;
44021
+ }
44022
+ this.reconnectDelay = RECONNECT_MIN_MS;
44023
+ this.connect();
43964
44024
  }
43965
44025
  engage() {
43966
44026
  this.send({ type: "engage" });
@@ -60671,7 +60731,7 @@ function ClickToCallAgent({ enabled = true, pollIntervalMs, sessionToken, onTask
60671
60731
  }, [enabled, pollIntervalMs, sessionToken]);
60672
60732
  return null;
60673
60733
  }
60674
- const WIDGET_VERSION = "2.7.0";
60734
+ const WIDGET_VERSION = "2.8.0";
60675
60735
  let currentSipWebSocket = null;
60676
60736
  function sendSipUnregisterIfPossible(ws) {
60677
60737
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -61070,7 +61130,10 @@ function DialerWrapper({ options, containerEl }) {
61070
61130
  current == null ? void 0 : current.close();
61071
61131
  const client2 = new EngineClient({
61072
61132
  url: options.engineUrl ?? "",
61073
- token: options.engineToken ?? "",
61133
+ token: options.engineToken || void 0,
61134
+ // credencial da aba: o token da sessão do Callix guardado neste navegador (lido a cada
61135
+ // hello, para a reconexão depois de uma troca de sessão pegar o token novo)
61136
+ sessionToken: () => readStoredToken(user),
61074
61137
  agentId: user,
61075
61138
  label: "Discador",
61076
61139
  log: (message) => console.log("[Callix Dialer/motor]", message)
@@ -61227,8 +61290,26 @@ function DialerWrapper({ options, containerEl }) {
61227
61290
  setEngineWaiting(!client2.getState().view);
61228
61291
  const fallback = setTimeout(() => setEngineWaiting(false), 15e3);
61229
61292
  let joining = false;
61293
+ let rejectedNote = false;
61230
61294
  const sync = () => {
61231
- const { view, sessionLost } = client2.getState();
61295
+ const { view, sessionLost, rejected } = client2.getState();
61296
+ if (!rejected && rejectedNote) {
61297
+ rejectedNote = false;
61298
+ setTabNote(null);
61299
+ }
61300
+ if (rejected) {
61301
+ rejectedNote = true;
61302
+ setEngineWaiting(false);
61303
+ if (isLoadingRef.current) return;
61304
+ if (isAuthenticatedRef.current) {
61305
+ console.log("[Callix Dialer] o motor recusou esta aba: a sessão do agente foi trocada por um login em outro lugar");
61306
+ setIsAuthenticated(false);
61307
+ setSessionToken(null);
61308
+ setCallixDomain(null);
61309
+ }
61310
+ setTabNote(rejected.message);
61311
+ return;
61312
+ }
61232
61313
  if (!view) return;
61233
61314
  setEngineWaiting(false);
61234
61315
  if (view.engine === "stopped" || view.engine === "crashed") logoffPendingRef.current = false;
@@ -61412,9 +61493,15 @@ function DialerWrapper({ options, containerEl }) {
61412
61493
  setLoginStatus("Conectando ao motor...");
61413
61494
  logoffPendingRef.current = false;
61414
61495
  const client2 = ensureEngine(user);
61415
- const view = await client2.waitForView(15e3);
61496
+ let view = null;
61497
+ try {
61498
+ view = await client2.waitForView(15e3);
61499
+ } catch (err) {
61500
+ if (!(err instanceof EngineAccessError)) throw err;
61501
+ console.log("[Callix Dialer] o motor recusou a credencial desta aba; fazendo login novo que substitui a sessão de lá");
61502
+ }
61416
61503
  const domain = engineDomain();
61417
- const engineHasSession = (view.engine === "running" || view.engine === "starting") && !view.sessionLost;
61504
+ const engineHasSession = view !== null && (view.engine === "running" || view.engine === "starting") && !view.sessionLost;
61418
61505
  let token = null;
61419
61506
  if (engineHasSession) {
61420
61507
  token = readStoredToken(user);
@@ -61433,8 +61520,18 @@ function DialerWrapper({ options, containerEl }) {
61433
61520
  }
61434
61521
  const data = await response.json();
61435
61522
  token = data.userSessionToken;
61523
+ try {
61524
+ const STORAGE_KEYS = getStorageKeys(user);
61525
+ localStorage.setItem(LAST_USERNAME_KEY, user);
61526
+ localStorage.setItem(STORAGE_KEYS.SESSION_TOKEN, token);
61527
+ localStorage.setItem(STORAGE_KEYS.DOMAIN, domain);
61528
+ localStorage.setItem(STORAGE_KEYS.TIMESTAMP, Date.now().toString());
61529
+ } catch {
61530
+ }
61436
61531
  client2.setSession({ domain, userSessionToken: token });
61437
61532
  setLoginStatus("Subindo o motor...");
61533
+ const up = await client2.waitForView(2e4, (v) => v.engine === "running" || v.engine === "starting" || Boolean(v.sessionLost), { fresh: true });
61534
+ if (up.sessionLost) throw new Error(up.sessionLost.message);
61438
61535
  }
61439
61536
  await enterEngine(user, domain, token);
61440
61537
  console.log("[Callix Dialer] Conectado ao motor como", user);