@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.
- package/README.md +64 -5
- package/dist/bravophone.mjs +419 -302
- package/dist/bravophone.umd.js +66 -7
- package/host/_locales/pt_BR/messages.json +1430 -0
- package/host/allowed-origins.json +8 -0
- package/host/css/bravophoneContactDialer.css +385 -0
- package/host/css/dark-theme.css +116 -0
- package/host/favicon.ico +0 -0
- package/host/fonts/Audiowide-Regular.ttf +0 -0
- package/host/fonts/Seguiemj.ttf +0 -0
- package/host/images/answer-gray.png +0 -0
- package/host/images/answer.png +0 -0
- package/host/images/auto-answer-off.png +0 -0
- package/host/images/auto-answer-on.png +0 -0
- package/host/images/avatar.png +0 -0
- package/host/images/bravophone-logo.svg +39 -0
- package/host/images/bravotech_Bravotech_logo.png +0 -0
- package/host/images/circle-green.png +0 -0
- package/host/images/circle-orange.png +0 -0
- package/host/images/circle-red.png +0 -0
- package/host/images/dialer-button-down.png +0 -0
- package/host/images/dialer-button.png +0 -0
- package/host/images/gateways/activecampaign.png +0 -0
- package/host/images/gateways/agendor.png +0 -0
- package/host/images/gateways/appfacilita.png +0 -0
- package/host/images/gateways/call-in.png +0 -0
- package/host/images/gateways/click-to-call.png +0 -0
- package/host/images/gateways/clickup.png +0 -0
- package/host/images/gateways/exactsales.png +0 -0
- package/host/images/gateways/go-high-lvl.png +0 -0
- package/host/images/gateways/groner.png +0 -0
- package/host/images/gateways/hubspot.png +0 -0
- package/host/images/gateways/kommo.png +0 -0
- package/host/images/gateways/moskit.png +0 -0
- package/host/images/gateways/nectar.png +0 -0
- package/host/images/gateways/pipedrive.png +0 -0
- package/host/images/gateways/pipefy.png +0 -0
- package/host/images/gateways/piperun.png +0 -0
- package/host/images/gateways/ploomes.png +0 -0
- package/host/images/gateways/rdstation-crm.png +0 -0
- package/host/images/gateways/salesforce.png +0 -0
- package/host/images/gateways/sharpspring.png +0 -0
- package/host/images/gateways/sunhub.png +0 -0
- package/host/images/gateways/vendfly.png +0 -0
- package/host/images/gateways/zendesk.png +0 -0
- package/host/images/gateways/zoho.png +0 -0
- package/host/images/hangup.png +0 -0
- package/host/images/hold.png +0 -0
- package/host/images/icons/logo-128-christmas-celebration.png +0 -0
- package/host/images/icons/logo-128.png +0 -0
- package/host/images/icons/logo-16-christmas-celebration.png +0 -0
- package/host/images/icons/logo-16.png +0 -0
- package/host/images/icons/logo-24-christmas-celebration.png +0 -0
- package/host/images/icons/logo-24.png +0 -0
- package/host/images/icons/logo-32-christmas-celebration.png +0 -0
- package/host/images/icons/logo-32.png +0 -0
- package/host/images/icons/logo-48-christmas-celebration.png +0 -0
- package/host/images/icons/logo-48.png +0 -0
- package/host/images/loading.gif +0 -0
- package/host/images/logo-bravotech-white.png +0 -0
- package/host/images/logo-bravotech.png +0 -0
- package/host/images/logo-inline-christmas-celebration.png +0 -0
- package/host/images/logo-inline.png +0 -0
- package/host/images/mic-off.png +0 -0
- package/host/images/mic-on.png +0 -0
- package/host/images/play.png +0 -0
- package/host/images/transfer.png +0 -0
- package/host/index.html +34 -0
- package/host/js/bravophone-noise-suppressor.js +418 -0
- package/host/js/bravophone-route-selector.js +1099 -0
- package/host/js/libwebphone.js +20 -0
- package/host/js/noise/LICENSE-web-noise-suppressor.txt +21 -0
- package/host/js/noise/rnnoise-worklet.js +13 -0
- package/host/js/noise/rnnoise.wasm +0 -0
- package/host/js/noise/rnnoise_simd.wasm +0 -0
- package/host/mock-webphone.js +195 -0
- package/host/mock.html +54 -0
- package/host/popup.js +23 -0
- package/host/shim/chrome-shim.js +287 -0
- package/host/shim/guest-bridge.js +353 -0
- package/host/shim/messages.js +13 -0
- package/host/styles/theme-fixes.css +98 -0
- package/package.json +4 -2
- 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
|
+
})()
|