@bravophone/webphone 0.1.2 → 0.2.1

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.
Files changed (84) hide show
  1. package/README.md +64 -5
  2. package/dist/bravophone.mjs +419 -302
  3. package/dist/bravophone.umd.js +66 -7
  4. package/host/_locales/pt_BR/messages.json +1430 -0
  5. package/host/allowed-origins.json +8 -0
  6. package/host/css/bravophoneContactDialer.css +385 -0
  7. package/host/css/dark-theme.css +116 -0
  8. package/host/favicon.ico +0 -0
  9. package/host/fonts/Audiowide-Regular.ttf +0 -0
  10. package/host/fonts/Seguiemj.ttf +0 -0
  11. package/host/images/answer-gray.png +0 -0
  12. package/host/images/answer.png +0 -0
  13. package/host/images/auto-answer-off.png +0 -0
  14. package/host/images/auto-answer-on.png +0 -0
  15. package/host/images/avatar.png +0 -0
  16. package/host/images/bravophone-logo.svg +39 -0
  17. package/host/images/bravotech_Bravotech_logo.png +0 -0
  18. package/host/images/circle-green.png +0 -0
  19. package/host/images/circle-orange.png +0 -0
  20. package/host/images/circle-red.png +0 -0
  21. package/host/images/dialer-button-down.png +0 -0
  22. package/host/images/dialer-button.png +0 -0
  23. package/host/images/gateways/activecampaign.png +0 -0
  24. package/host/images/gateways/agendor.png +0 -0
  25. package/host/images/gateways/appfacilita.png +0 -0
  26. package/host/images/gateways/call-in.png +0 -0
  27. package/host/images/gateways/click-to-call.png +0 -0
  28. package/host/images/gateways/clickup.png +0 -0
  29. package/host/images/gateways/exactsales.png +0 -0
  30. package/host/images/gateways/go-high-lvl.png +0 -0
  31. package/host/images/gateways/groner.png +0 -0
  32. package/host/images/gateways/hubspot.png +0 -0
  33. package/host/images/gateways/kommo.png +0 -0
  34. package/host/images/gateways/moskit.png +0 -0
  35. package/host/images/gateways/nectar.png +0 -0
  36. package/host/images/gateways/pipedrive.png +0 -0
  37. package/host/images/gateways/pipefy.png +0 -0
  38. package/host/images/gateways/piperun.png +0 -0
  39. package/host/images/gateways/ploomes.png +0 -0
  40. package/host/images/gateways/rdstation-crm.png +0 -0
  41. package/host/images/gateways/salesforce.png +0 -0
  42. package/host/images/gateways/sharpspring.png +0 -0
  43. package/host/images/gateways/sunhub.png +0 -0
  44. package/host/images/gateways/vendfly.png +0 -0
  45. package/host/images/gateways/zendesk.png +0 -0
  46. package/host/images/gateways/zoho.png +0 -0
  47. package/host/images/hangup.png +0 -0
  48. package/host/images/hold.png +0 -0
  49. package/host/images/icons/logo-128-christmas-celebration.png +0 -0
  50. package/host/images/icons/logo-128.png +0 -0
  51. package/host/images/icons/logo-16-christmas-celebration.png +0 -0
  52. package/host/images/icons/logo-16.png +0 -0
  53. package/host/images/icons/logo-24-christmas-celebration.png +0 -0
  54. package/host/images/icons/logo-24.png +0 -0
  55. package/host/images/icons/logo-32-christmas-celebration.png +0 -0
  56. package/host/images/icons/logo-32.png +0 -0
  57. package/host/images/icons/logo-48-christmas-celebration.png +0 -0
  58. package/host/images/icons/logo-48.png +0 -0
  59. package/host/images/loading.gif +0 -0
  60. package/host/images/logo-bravotech-white.png +0 -0
  61. package/host/images/logo-bravotech.png +0 -0
  62. package/host/images/logo-inline-christmas-celebration.png +0 -0
  63. package/host/images/logo-inline.png +0 -0
  64. package/host/images/mic-off.png +0 -0
  65. package/host/images/mic-on.png +0 -0
  66. package/host/images/play.png +0 -0
  67. package/host/images/transfer.png +0 -0
  68. package/host/index.html +34 -0
  69. package/host/js/bravophone-noise-suppressor.js +418 -0
  70. package/host/js/bravophone-route-selector.js +1099 -0
  71. package/host/js/libwebphone.js +20 -0
  72. package/host/js/noise/LICENSE-web-noise-suppressor.txt +21 -0
  73. package/host/js/noise/rnnoise-worklet.js +13 -0
  74. package/host/js/noise/rnnoise.wasm +0 -0
  75. package/host/js/noise/rnnoise_simd.wasm +0 -0
  76. package/host/mock-webphone.js +195 -0
  77. package/host/mock.html +54 -0
  78. package/host/popup.js +23 -0
  79. package/host/shim/chrome-shim.js +287 -0
  80. package/host/shim/guest-bridge.js +353 -0
  81. package/host/shim/messages.js +13 -0
  82. package/host/styles/theme-fixes.css +98 -0
  83. package/package.json +4 -2
  84. package/types/index.d.ts +30 -2
@@ -0,0 +1,287 @@
1
+ /*!
2
+ * chrome-shim.js — implementa a superfície de `chrome.*` que o bundle do
3
+ * webphone usa, para que popup.js rode numa página comum, fora da extensão.
4
+ *
5
+ * PRECISA carregar ANTES de libwebphone.js e popup.js.
6
+ *
7
+ * Superfície coberta (levantada do popup.js / bravophone-route-selector.js /
8
+ * bravophone-noise-suppressor.js da extensão):
9
+ * chrome.storage.sync|local|session .get .set .remove .clear .onChanged
10
+ * chrome.runtime .onMessage .sendMessage .lastError .getURL .id
11
+ * chrome.tabs .create .sendMessage
12
+ * chrome.windows .create .update .get .getCurrent
13
+ */
14
+ ;(function () {
15
+ 'use strict'
16
+ // Se estivermos dentro da extensão real, não toca em nada.
17
+ if (window.chrome && window.chrome.storage && window.chrome.runtime && window.chrome.runtime.id) return
18
+
19
+ var PREFIX = 'bp.'
20
+ var listeners = { storage: [], message: [] }
21
+
22
+ // Aceita callback OU Promise, como a API MV3 faz.
23
+ function settle(cb, value) {
24
+ if (typeof cb === 'function') { cb(value); return Promise.resolve(value) }
25
+ return Promise.resolve(value)
26
+ }
27
+
28
+ function makeArea(area) {
29
+ var ns = PREFIX + area + '.'
30
+
31
+ function readKey(key) {
32
+ var raw = localStorage.getItem(ns + key)
33
+ if (raw === null) return undefined
34
+ try { return JSON.parse(raw) } catch (_) { return raw }
35
+ }
36
+
37
+ function allKeys() {
38
+ var out = []
39
+ for (var i = 0; i < localStorage.length; i++) {
40
+ var k = localStorage.key(i)
41
+ if (k && k.indexOf(ns) === 0) out.push(k.slice(ns.length))
42
+ }
43
+ return out
44
+ }
45
+
46
+ return {
47
+ get: function (keys, cb) {
48
+ if (typeof keys === 'function') { cb = keys; keys = null }
49
+ var result = {}
50
+ try {
51
+ if (keys === null || keys === undefined) {
52
+ allKeys().forEach(function (k) { result[k] = readKey(k) })
53
+ } else if (typeof keys === 'string') {
54
+ var v = readKey(keys)
55
+ if (v !== undefined) result[keys] = v
56
+ } else if (Array.isArray(keys)) {
57
+ keys.forEach(function (k) {
58
+ var val = readKey(k)
59
+ if (val !== undefined) result[k] = val
60
+ })
61
+ } else if (typeof keys === 'object') {
62
+ // objeto = mapa de defaults
63
+ Object.keys(keys).forEach(function (k) {
64
+ var val = readKey(k)
65
+ result[k] = val === undefined ? keys[k] : val
66
+ })
67
+ }
68
+ } catch (err) { console.warn('[bp-shim] storage.get:', err) }
69
+ return settle(cb, result)
70
+ },
71
+
72
+ set: function (items, cb) {
73
+ var changes = {}
74
+ try {
75
+ Object.keys(items || {}).forEach(function (k) {
76
+ var oldValue = readKey(k)
77
+ localStorage.setItem(ns + k, JSON.stringify(items[k]))
78
+ changes[k] = { oldValue: oldValue, newValue: items[k] }
79
+ })
80
+ } catch (err) {
81
+ // Cota estourada é o caso real aqui (localStorage ~5 MB por origem).
82
+ console.warn('[bp-shim] storage.set:', err)
83
+ }
84
+ fireStorage(changes, area)
85
+ return settle(cb, undefined)
86
+ },
87
+
88
+ remove: function (keys, cb) {
89
+ var list = Array.isArray(keys) ? keys : [keys]
90
+ var changes = {}
91
+ list.forEach(function (k) {
92
+ changes[k] = { oldValue: readKey(k) }
93
+ localStorage.removeItem(ns + k)
94
+ })
95
+ fireStorage(changes, area)
96
+ return settle(cb, undefined)
97
+ },
98
+
99
+ clear: function (cb) {
100
+ var changes = {}
101
+ allKeys().forEach(function (k) {
102
+ changes[k] = { oldValue: readKey(k) }
103
+ localStorage.removeItem(ns + k)
104
+ })
105
+ fireStorage(changes, area)
106
+ return settle(cb, undefined)
107
+ },
108
+ }
109
+ }
110
+
111
+ function fireStorage(changes, area) {
112
+ if (!changes || !Object.keys(changes).length) return
113
+ listeners.storage.forEach(function (fn) {
114
+ try { fn(changes, area) } catch (err) { console.error('[bp-shim] onChanged:', err) }
115
+ })
116
+ }
117
+
118
+ function makeEvent(bucket) {
119
+ return {
120
+ addListener: function (fn) { if (bucket.indexOf(fn) === -1) bucket.push(fn) },
121
+ removeListener: function (fn) {
122
+ var i = bucket.indexOf(fn)
123
+ if (i !== -1) bucket.splice(i, 1)
124
+ },
125
+ hasListener: function (fn) { return bucket.indexOf(fn) !== -1 },
126
+ hasListeners: function () { return bucket.length > 0 },
127
+ }
128
+ }
129
+
130
+ function bridgeEmit(name, payload) {
131
+ if (window.__bpBridge && typeof window.__bpBridge.emit === 'function') {
132
+ window.__bpBridge.emit(name, payload)
133
+ }
134
+ }
135
+
136
+ var chromeShim = {
137
+ storage: {
138
+ sync: makeArea('sync'),
139
+ local: makeArea('local'),
140
+ session: makeArea('session'),
141
+ onChanged: makeEvent(listeners.storage),
142
+ },
143
+
144
+ runtime: {
145
+ id: 'bravophone-embed',
146
+ // lastError sempre undefined: sem IPC, não há erro assíncrono de canal.
147
+ lastError: undefined,
148
+ getURL: function (path) {
149
+ return new URL(String(path).replace(/^\/+/, ''), document.baseURI).href
150
+ },
151
+ onMessage: makeEvent(listeners.message),
152
+ sendMessage: function (a, b, c) {
153
+ // Assinaturas: (msg), (msg, cb), (extId, msg), (extId, msg, cb)
154
+ var msg = typeof a === 'string' && b !== undefined ? b : a
155
+ var cb = typeof c === 'function' ? c : typeof b === 'function' ? b : undefined
156
+ var responded = false
157
+ var reply = function (r) { if (!responded) { responded = true; if (cb) cb(r) } }
158
+ listeners.message.forEach(function (fn) {
159
+ try { fn(msg, { id: 'bravophone-embed', url: location.href }, reply) } catch (err) {
160
+ console.error('[bp-shim] onMessage:', err)
161
+ }
162
+ })
163
+ if (!responded) reply(undefined)
164
+ return Promise.resolve(undefined)
165
+ },
166
+ connect: function () {
167
+ return {
168
+ postMessage: function () {},
169
+ disconnect: function () {},
170
+ onMessage: makeEvent([]),
171
+ onDisconnect: makeEvent([]),
172
+ }
173
+ },
174
+ getManifest: function () { return { version: '0.1.0', name: 'Bravophone Embed' } },
175
+ },
176
+
177
+ tabs: {
178
+ // No embed não há abas: abrir link vira uma nova aba do browser mesmo.
179
+ create: function (props, cb) {
180
+ var win = window.open(props && props.url, '_blank', 'noopener,noreferrer')
181
+ return settle(cb, { id: Date.now(), url: props && props.url, window: win ? 1 : 0 })
182
+ },
183
+ query: function (_q, cb) { return settle(cb, []) },
184
+ sendMessage: function (_tabId, msg, cb) { return chromeShim.runtime.sendMessage(msg, cb) },
185
+ update: function (_id, _props, cb) { return settle(cb, {}) },
186
+ },
187
+
188
+ windows: {
189
+ // A "janela" agora é o widget arrastável no host: estes stubs mantêm o
190
+ // bundle funcionando e delegam ao host o que faz sentido.
191
+ WINDOW_ID_CURRENT: -2,
192
+ create: function (props, cb) {
193
+ bridgeEmit('window:create', props)
194
+ return settle(cb, Object.assign({ id: 1 }, props))
195
+ },
196
+ update: function (id, props, cb) {
197
+ if (props && (props.width || props.height)) {
198
+ bridgeEmit('window:resize', { width: props.width, height: props.height })
199
+ }
200
+ if (props && props.focused) bridgeEmit('window:focus', {})
201
+ return settle(cb, Object.assign({ id: id }, props))
202
+ },
203
+ get: function (id, info, cb) {
204
+ var callback = typeof info === 'function' ? info : cb
205
+ return settle(callback, { id: id, width: window.innerWidth, height: window.innerHeight })
206
+ },
207
+ getCurrent: function (info, cb) {
208
+ var callback = typeof info === 'function' ? info : cb
209
+ return settle(callback, {
210
+ id: 1,
211
+ width: window.innerWidth,
212
+ height: window.innerHeight,
213
+ focused: document.hasFocus(),
214
+ })
215
+ },
216
+ remove: function (_id, cb) {
217
+ bridgeEmit('window:close', {})
218
+ return settle(cb, undefined)
219
+ },
220
+ },
221
+
222
+ contextMenus: { create: function () {}, removeAll: function (cb) { return settle(cb, undefined) } },
223
+ scripting: { executeScript: function (_o, cb) { return settle(cb, []) } },
224
+ action: {
225
+ setBadgeText: function (_o, cb) { return settle(cb, undefined) },
226
+ setIcon: function (_o, cb) { return settle(cb, undefined) },
227
+ },
228
+ i18n: {
229
+ /**
230
+ * As mensagens vêm de shim/messages.js, gerado do _locales/ da extensão.
231
+ * O bundle expõe `t(k){ return chrome.i18n.getMessage(k) }` e os
232
+ * templates usam isso para quase todo label — sem estas mensagens a
233
+ * interface renderiza sem texto nenhum.
234
+ */
235
+ getMessage: function (key, substitutions) {
236
+ var table = window.__bpMessages
237
+ if (!table) {
238
+ if (!chromeShim.i18n._warned) {
239
+ chromeShim.i18n._warned = true
240
+ console.error('[bp-shim] __bpMessages ausente: a UI vai aparecer sem textos. ' +
241
+ 'Rode `npm run sync` para gerar shim/messages.js.')
242
+ }
243
+ return ''
244
+ }
245
+ var entry = table[key]
246
+ if (!entry) return ''
247
+ var msg = entry.message || ''
248
+
249
+ // Placeholders nomeados do messages.json: "$USER$" -> content "$1".
250
+ if (entry.placeholders) {
251
+ Object.keys(entry.placeholders).forEach(function (name) {
252
+ var ph = entry.placeholders[name]
253
+ // A string precisa levar DUAS barras: '\\$' em JS produz '\$' na
254
+ // regex, que é o cifrão literal. Com uma barra só, o $ chega cru à
255
+ // regex e vira âncora de fim de linha — nada é substituído.
256
+ var safe = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
257
+ msg = msg.replace(
258
+ new RegExp('\\$' + safe + '\\$', 'gi'),
259
+ ph && ph.content != null ? String(ph.content) : ''
260
+ )
261
+ })
262
+ }
263
+
264
+ // Substituições posicionais: $1..$9.
265
+ if (substitutions != null) {
266
+ var list = Array.isArray(substitutions) ? substitutions : [substitutions]
267
+ msg = msg.replace(/\$([1-9])/g, function (whole, i) {
268
+ var v = list[Number(i) - 1]
269
+ return v == null ? '' : String(v)
270
+ })
271
+ }
272
+
273
+ // "$$" escapa um cifrão literal.
274
+ return msg.replace(/\$\$/g, '$')
275
+ },
276
+ getUILanguage: function () { return window.__bpLocale || navigator.language || 'pt-BR' },
277
+ getAcceptLanguages: function (cb) { return settle(cb, [navigator.language || 'pt-BR']) },
278
+ },
279
+ }
280
+
281
+ window.chrome = window.chrome || {}
282
+ Object.keys(chromeShim).forEach(function (k) {
283
+ if (!window.chrome[k]) window.chrome[k] = chromeShim[k]
284
+ })
285
+ window.browser = window.browser || window.chrome
286
+ window.__bpShimActive = true
287
+ })()
@@ -0,0 +1,353 @@
1
+ /*!
2
+ * guest-bridge.js — lado iframe do canal postMessage.
3
+ *
4
+ * Carrega DEPOIS de popup.js. Faz duas coisas:
5
+ * 1. traduz comandos vindos do site (call, hangup, mute...) em ações no
6
+ * webphone, usando os mesmos pontos de entrada que a extensão usa;
7
+ * 2. publica eventos de telefonia de volta para o site.
8
+ *
9
+ * Para discar usamos a mesma porta que os content-scripts de CRM usam no
10
+ * click-to-call: chrome.runtime.onMessage com method 'webphoneDialNow'. Ela
11
+ * desemboca em 'webphone-dial-from-pad', o funil único de ligações — então
12
+ * normalização de número, seleção de rota e registro de origem continuam
13
+ * valendo exatamente como na extensão.
14
+ */
15
+ ;(function () {
16
+ 'use strict'
17
+
18
+ var PROTOCOL = 'bravophone/v1'
19
+ var parentOrigin = null
20
+
21
+ // A origem do pai chega de dois jeitos, conforme o modo:
22
+ // · hospedado — query string (?parent=...), pois o iframe navega
23
+ // · srcdoc — variável injetada, pois não há URL própria para carregar
24
+ // Nos dois casos o servidor ainda valida a origem pela allowlist; isto aqui
25
+ // sozinho nunca é garantia de nada.
26
+ try {
27
+ parentOrigin = window.__bpParentOrigin ||
28
+ new URLSearchParams(location.search).get('parent')
29
+ } catch (_) {}
30
+
31
+ if (!parentOrigin || window.parent === window) {
32
+ console.warn('[bp-bridge] sem origem pai; rodando standalone.')
33
+ return
34
+ }
35
+
36
+ function post(msg) {
37
+ try {
38
+ window.parent.postMessage(Object.assign({ protocol: PROTOCOL }, msg), parentOrigin)
39
+ } catch (err) {
40
+ console.error('[bp-bridge] postMessage falhou:', err)
41
+ }
42
+ }
43
+
44
+ var bridge = {
45
+ emit: function (name, payload) { post({ type: 'event', name: name, payload: payload }) },
46
+ }
47
+ window.__bpBridge = bridge
48
+
49
+ // --- espera o app Vue montar e expor os handles ---
50
+ function waitFor(getter, timeout) {
51
+ return new Promise(function (resolve, reject) {
52
+ var started = Date.now()
53
+ ;(function tick() {
54
+ var v
55
+ try { v = getter() } catch (_) {}
56
+ if (v) return resolve(v)
57
+ if (Date.now() - started > (timeout || 20000)) {
58
+ return reject(new Error('Bravophone: webphone não inicializou a tempo'))
59
+ }
60
+ setTimeout(tick, 120)
61
+ })()
62
+ })
63
+ }
64
+
65
+ // --- como se fala com o bundle -------------------------------------------
66
+ //
67
+ // O popup.js NÃO expõe nada em window. Há dois caminhos de verdade, ambos
68
+ // levantados do próprio bundle:
69
+ //
70
+ // 1. DISCAR — chrome.runtime.onMessage com method 'webphoneDialNow'. É a
71
+ // mesma porta que os content-scripts de CRM usam para click-to-call, e
72
+ // por dentro ela emite 'webphone-dial-from-pad', o funil único de
73
+ // ligações: normalização de número, seleção de rota e registro de
74
+ // origem continuam valendo iguais aos da extensão.
75
+ //
76
+ // 2. AÇÕES EM CHAMADA — o emitter (mitt) que o app publica via
77
+ // `app.provide(...)`: call-answer, call-hangup, call-toggle-mute,
78
+ // call-toggle-hold.
79
+ //
80
+ // O emitter é achado pela FORMA (um objeto com on/off/emit), não pelo nome
81
+ // da chave — o bundle é minificado e a chave muda a cada build.
82
+
83
+ function vueApp() {
84
+ var root = document.getElementById('app')
85
+ return (root && root.__vue_app__) || null
86
+ }
87
+
88
+ function findEmitter() {
89
+ var app = vueApp()
90
+ if (!app) return null
91
+ var pools = []
92
+ if (app._context) {
93
+ if (app._context.provides) pools.push(app._context.provides)
94
+ if (app._context.config && app._context.config.globalProperties) {
95
+ pools.push(app._context.config.globalProperties)
96
+ }
97
+ }
98
+ if (app.config && app.config.globalProperties) pools.push(app.config.globalProperties)
99
+
100
+ for (var i = 0; i < pools.length; i++) {
101
+ var pool = pools[i]
102
+ for (var k in pool) {
103
+ var v = pool[k]
104
+ // Assinatura do mitt: { all, on, off, emit }
105
+ if (v && typeof v.emit === 'function' && typeof v.on === 'function' &&
106
+ typeof v.off === 'function') return v
107
+ }
108
+ }
109
+ return null
110
+ }
111
+
112
+ function findStore() {
113
+ var app = vueApp()
114
+ if (!app) return null
115
+ var pools = []
116
+ if (app._context && app._context.provides) pools.push(app._context.provides)
117
+ if (app.config && app.config.globalProperties) pools.push(app.config.globalProperties)
118
+ for (var i = 0; i < pools.length; i++) {
119
+ for (var k in pools[i]) {
120
+ var v = pools[i][k]
121
+ if (v && v.state && typeof v.commit === 'function') return v
122
+ }
123
+ }
124
+ return null
125
+ }
126
+
127
+ /** Emite no emitter do app; falha com mensagem clara se ele não aparecer. */
128
+ function emitApp(event, arg) {
129
+ return waitFor(findEmitter, 20000).then(function (em) {
130
+ em.emit(event, arg)
131
+ return { ok: true }
132
+ })
133
+ }
134
+
135
+ // --- comandos aceitos do site hospedeiro ---
136
+ var commands = {
137
+ auth: function (p) {
138
+ // Sessão do integrador: grava onde o bundle já procura o token.
139
+ if (!p || !p.token) throw new Error('token ausente')
140
+ return new Promise(function (resolve) {
141
+ chrome.storage.local.set({ vxToken: p.token }, function () {
142
+ chrome.storage.sync.set({ vxToken: p.token }, function () { resolve({ ok: true }) })
143
+ })
144
+ })
145
+ },
146
+
147
+ logout: function () {
148
+ return new Promise(function (resolve) {
149
+ chrome.storage.local.remove(['vxToken'], function () {
150
+ chrome.storage.sync.remove(['vxToken'], function () { resolve({ ok: true }) })
151
+ })
152
+ })
153
+ },
154
+
155
+ call: function (p) {
156
+ if (!p || !p.number) throw new Error('número ausente')
157
+ var meta = p.meta || {}
158
+ // Mesmo payload que a API pública da extensão monta para os CRMs.
159
+ var payload = {
160
+ phone: String(p.number),
161
+ name: meta.name || null,
162
+ crm: meta.crm || null,
163
+ photo: meta.photo || null,
164
+ gateway: meta.gateway || meta.source || 'sdk',
165
+ dealId: meta.dealId || null,
166
+ pipedriveUrl: meta.url || null,
167
+ }
168
+
169
+ return new Promise(function (resolve, reject) {
170
+ var answered = false
171
+ // O listener do popup só existe depois do app montar; se ainda não
172
+ // houver ninguém escutando, esperamos e reenviamos.
173
+ var attempts = 0
174
+ ;(function send() {
175
+ attempts++
176
+ if (!chrome.runtime.onMessage.hasListeners() && attempts < 60) {
177
+ return setTimeout(send, 200)
178
+ }
179
+ if (!chrome.runtime.onMessage.hasListeners()) {
180
+ return reject(new Error('webphone não terminou de carregar'))
181
+ }
182
+ chrome.runtime.sendMessage({ method: 'webphoneDialNow', payload: payload }, function () {
183
+ if (answered) return
184
+ answered = true
185
+ resolve({ ok: true, phone: payload.phone })
186
+ })
187
+ // sendMessage responde de forma síncrona no shim; este timeout só
188
+ // cobre o caso de nenhum listener responder.
189
+ setTimeout(function () {
190
+ if (!answered) { answered = true; resolve({ ok: true, phone: payload.phone }) }
191
+ }, 300)
192
+ })()
193
+ })
194
+ },
195
+
196
+ hangup: function () { return emitApp('call-hangup') },
197
+ answer: function () { return emitApp('call-answer') },
198
+ // mute/hold do bundle são TOGGLE: não aceitam estado alvo.
199
+ mute: function () { return emitApp('call-toggle-mute') },
200
+ hold: function () { return emitApp('call-toggle-hold') },
201
+ dtmf: function (p) {
202
+ if (!p || p.tone == null) throw new Error('tom ausente')
203
+ return emitApp('webphone-dial-key', String(p.tone))
204
+ },
205
+ transfer: function (p) {
206
+ if (!p || !p.to) throw new Error('destino ausente')
207
+ return emitApp('webphone-transfer-initiated', String(p.to))
208
+ },
209
+ clearDial: function () { return emitApp('webphone-dial-clear') },
210
+
211
+ status: function () {
212
+ var store = findStore()
213
+ var st = (store && store.state) || {}
214
+ return Promise.resolve({
215
+ ready: !!findEmitter(),
216
+ inCall: !!(st.callActive || st.callPhase),
217
+ phase: st.callPhase || null,
218
+ number: st.callDisplay || null,
219
+ incoming: !!st.callIsIncoming,
220
+ muted: !!st.callMuted,
221
+ held: !!st.callHeld,
222
+ })
223
+ },
224
+
225
+ ping: function () { return Promise.resolve({ pong: Date.now() }) },
226
+ }
227
+
228
+ function ok() { return { ok: true } }
229
+
230
+ window.addEventListener('message', function (ev) {
231
+ if (ev.origin !== parentOrigin) return
232
+ if (ev.source !== window.parent) return
233
+ var msg = ev.data
234
+ if (!msg || msg.protocol !== PROTOCOL || msg.type !== 'command') return
235
+
236
+ var fn = commands[msg.command]
237
+ if (!fn) {
238
+ post({ type: 'reply', id: msg.id, error: 'comando desconhecido: ' + msg.command })
239
+ return
240
+ }
241
+ Promise.resolve()
242
+ .then(function () { return fn(msg.payload || {}) })
243
+ .then(function (payload) { post({ type: 'reply', id: msg.id, payload: payload }) })
244
+ .catch(function (err) { post({ type: 'reply', id: msg.id, error: String(err && err.message || err) }) })
245
+ })
246
+
247
+ // --- eventos de telefonia -> site ----------------------------------------
248
+ //
249
+ // Não há um `libwebphone` global para escutar. O que existe é o store Vuex,
250
+ // onde o app mantém o estado da chamada (callActive, callPhase, callDisplay,
251
+ // callIsIncoming…). Observamos esse estado e traduzimos as TRANSIÇÕES em
252
+ // eventos — derivar de estado é mais estável que caçar nomes de evento
253
+ // internos, que mudam a cada build do bundle.
254
+ ;(function watchCallState() {
255
+ var prev = null
256
+
257
+ function snapshot(st) {
258
+ return {
259
+ active: !!(st.callActive || st.callPhase),
260
+ phase: st.callPhase || null,
261
+ incoming: !!st.callIsIncoming,
262
+ number: st.callDisplay || null,
263
+ }
264
+ }
265
+
266
+ function describe(s) {
267
+ return { id: null, number: s.number, direction: s.incoming ? 'inbound' : 'outbound' }
268
+ }
269
+
270
+ function tick(store) {
271
+ var now = snapshot(store.state || {})
272
+
273
+ if (!prev) { prev = now; return }
274
+
275
+ // Começou uma chamada.
276
+ if (now.active && !prev.active) {
277
+ bridge.emit(now.incoming ? 'call:incoming' : 'call:dialing', describe(now))
278
+ }
279
+ // Foi atendida: sai de ringing/dialing para um estado de conversa.
280
+ if (now.active && prev.active && now.phase !== prev.phase &&
281
+ /answer|establish|active|talking|confirmed/i.test(String(now.phase))) {
282
+ bridge.emit('call:answered', describe(now))
283
+ }
284
+ // Terminou.
285
+ if (!now.active && prev.active) {
286
+ bridge.emit('call:ended', describe(prev))
287
+ }
288
+ prev = now
289
+ }
290
+
291
+ waitFor(findStore, 30000).then(function (store) {
292
+ bridge.emit('state', { state: 'ready' })
293
+ if (typeof store.subscribe === 'function') {
294
+ // subscribe dispara a cada mutation: reage na hora, sem polling.
295
+ store.subscribe(function () { tick(store) })
296
+ }
297
+ // Rede de segurança: nem toda mudança relevante passa por mutation.
298
+ setInterval(function () { tick(store) }, 500)
299
+ }).catch(function (err) {
300
+ console.warn('[bp-bridge] store não encontrado; eventos de chamada ficam indisponíveis:', err.message)
301
+ bridge.emit('error', { message: 'estado de chamada indisponível: ' + err.message })
302
+ })
303
+ })()
304
+
305
+ // --- arraste a partir de DENTRO do iframe ---------------------------------
306
+ // O host é cross-origin e não pode tocar neste DOM, então a detecção do
307
+ // arraste tem de acontecer aqui e viajar como delta pela ponte. É isso que
308
+ // permite arrastar o webphone pela própria UI, sem barra de título externa.
309
+ ;(function enableInnerDrag() {
310
+ // Qualquer coisa clicável continua clicável: só o "fundo" arrasta.
311
+ var INTERACTIVE = 'input,button,a,select,textarea,label,option,[contenteditable],[role="button"],[role="tab"],[role="link"],[draggable="true"]'
312
+ var dragging = false
313
+
314
+ document.addEventListener('pointerdown', function (ev) {
315
+ if (ev.button !== 0) return
316
+ if (!ev.target || (ev.target.closest && ev.target.closest(INTERACTIVE))) return
317
+ // Se o usuário está selecionando texto, não sequestra o gesto.
318
+ var sel = window.getSelection && window.getSelection()
319
+ if (sel && String(sel).length) return
320
+
321
+ dragging = true
322
+ // cx/cy: posicao dentro do iframe. O host soma a posicao da janela e
323
+ // descobre onde o cursor esta na viewport dele — e nao pode calcular
324
+ // isso sozinho, por ser cross-origin.
325
+ bridge.emit('drag:start', { x: ev.screenX, y: ev.screenY, cx: ev.clientX, cy: ev.clientY })
326
+ try { document.documentElement.setPointerCapture(ev.pointerId) } catch (_) {}
327
+ }, true)
328
+
329
+ document.addEventListener('pointermove', function (ev) {
330
+ if (!dragging) return
331
+ // screenX/screenY são absolutos na tela: imunes ao próprio iframe se
332
+ // mover durante o arraste, o que clientX/clientY não seriam.
333
+ bridge.emit('drag:move', { x: ev.screenX, y: ev.screenY })
334
+ }, true)
335
+
336
+ function stop(ev) {
337
+ if (!dragging) return
338
+ dragging = false
339
+ bridge.emit('drag:end', {})
340
+ try { document.documentElement.releasePointerCapture(ev.pointerId) } catch (_) {}
341
+ }
342
+ document.addEventListener('pointerup', stop, true)
343
+ document.addEventListener('pointercancel', stop, true)
344
+ })()
345
+
346
+ // Sinaliza pronto assim que o DOM do app existir.
347
+ function announceReady() {
348
+ // Identifica o HOST, não o SDK: são artefatos com ciclos separados.
349
+ post({ type: 'ready', payload: { host: 'bravophone-embed' } })
350
+ }
351
+ if (document.readyState === 'complete') announceReady()
352
+ else window.addEventListener('load', announceReady)
353
+ })()