-
Notifications
You must be signed in to change notification settings - Fork 5
feat: narrow Wealth Management app and Symphony host integration #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,5 +4,6 @@ node_modules | |
| dist | ||
| coverage | ||
| .project | ||
| .vscode | ||
| build | ||
| package-lock.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| registry=https://registry.npmjs.org |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| module.exports = { | ||
| plugins: [ | ||
| require('tailwindcss'), | ||
| require('autoprefixer'), | ||
| ], | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,32 @@ | |||||||||||||||||||||||||||
| Learn how to configure a non-root public URL by running `npm run build`. | ||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||
| <title>Clever Deal 2.0</title> | ||||||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||||||
| Suppress cross-origin "Script error." events from the Symphony ECP SDK | ||||||||||||||||||||||||||||
| iframe. Must run before bundle.js so our listener is registered first | ||||||||||||||||||||||||||||
| and can call stopImmediatePropagation before CRA's overlay handler. | ||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||
| <script> | ||||||||||||||||||||||||||||
| (function () { | ||||||||||||||||||||||||||||
| if (!document.getElementById('symphony-ecm')) { | ||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| window.addEventListener( | ||||||||||||||||||||||||||||
| 'error', | ||||||||||||||||||||||||||||
| function (e) { | ||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+41
|
||||||||||||||||||||||||||||
| if (!document.getElementById('symphony-ecm')) { | |
| return; | |
| } | |
| window.addEventListener( | |
| 'error', | |
| function (e) { | |
| window.addEventListener( | |
| 'error', | |
| function (e) { | |
| if (!document.getElementById('symphony-ecm')) { | |
| return; | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,318 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Wealth Client Chat Host</title> | ||
| <style> | ||
| html, | ||
| body, | ||
| #wealth-client-chat { | ||
| height: 100%; | ||
| width: 100%; | ||
| margin: 0; | ||
| padding: 0; | ||
| overflow: hidden; | ||
| background: #fbfcfe; | ||
| } | ||
|
|
||
| iframe { | ||
| border: 0; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="wealth-client-chat"></div> | ||
| <script> | ||
| (function () { | ||
| const DEFAULT_ORIGIN = 'corporate.symphony.com'; | ||
| const DEFAULT_PARTNER_ID = 'symphony_internal_BYC-XXX'; | ||
| const CONTAINER_ID = 'wealth-client-chat'; | ||
| const ALLOWED_APPS = 'com.symphony.zoom,com.symphony.teams,salesforce2-app,com.symphony.sfs.admin-app'; | ||
| const query = new URLSearchParams(window.location.search); | ||
| const ecpOrigin = query.get('ecpOrigin') || DEFAULT_ORIGIN; | ||
| const partnerId = query.get('partnerId'); | ||
| const mode = query.get('mode') || 'light'; | ||
| const theme = query.get('theme'); | ||
| const podUrl = 'https://' + ecpOrigin; | ||
| const initialStreamId = query.get('streamId'); | ||
|
Comment on lines
+32
to
+38
|
||
| let activeStreamId = initialStreamId; | ||
| let frame = null; | ||
| let isClientReady = false; | ||
| let actionId = 0; | ||
| const pendingSendActions = {}; | ||
|
|
||
| function debug(message, context) { | ||
| if (context) { | ||
| console.debug('[WealthClientHost] ' + message, context); | ||
| return; | ||
| } | ||
|
|
||
| console.debug('[WealthClientHost] ' + message); | ||
| } | ||
|
|
||
| function warn(message, context) { | ||
| if (context) { | ||
| console.warn('[WealthClientHost] ' + message, context); | ||
| return; | ||
| } | ||
|
|
||
| console.warn('[WealthClientHost] ' + message); | ||
| } | ||
|
|
||
| function error(message, context) { | ||
| if (context) { | ||
| console.error('[WealthClientHost] ' + message, context); | ||
| return; | ||
| } | ||
|
|
||
| console.error('[WealthClientHost] ' + message); | ||
| } | ||
|
|
||
| function postToParent(type, payload) { | ||
| debug('Posting host event to parent.', { type: type, payload: payload || {} }); | ||
| window.parent.postMessage( | ||
| { | ||
| source: 'wealth-client-chat-host', | ||
| type: type, | ||
| payload: payload || {}, | ||
| }, | ||
| window.location.origin, | ||
| ); | ||
| } | ||
|
|
||
| function buildFrameUrl(streamId) { | ||
| const url = new URL('https://' + ecpOrigin + '/embed/index.html'); | ||
| url.searchParams.set('streamId', streamId); | ||
| url.searchParams.set('module', 'room'); | ||
| url.searchParams.set('mode', mode); | ||
| url.searchParams.set('condensed', 'false'); | ||
| url.searchParams.set('showTitle', 'false'); | ||
| url.searchParams.set('canAddPeople', 'true'); | ||
| url.searchParams.set('ecpLoginPopup', 'true'); | ||
| url.searchParams.set('allowedApps', ALLOWED_APPS); | ||
| url.searchParams.set('sound', 'false'); | ||
| url.searchParams.set('showInfo', 'false'); | ||
| url.searchParams.set('showMembers', 'false'); | ||
| url.searchParams.set('symphonyLogo', 'false'); | ||
| url.searchParams.set('sdkOrigin', window.location.origin); | ||
| url.searchParams.set('embed', 'true'); | ||
| if (theme) { | ||
| url.searchParams.set('theme', theme); | ||
| } | ||
| if (partnerId) { | ||
| url.searchParams.set('partnerId', partnerId); | ||
| } else if (ecpOrigin !== 'st3.dev.symphony.com') { | ||
| url.searchParams.set('partnerId', DEFAULT_PARTNER_ID); | ||
| } | ||
| debug('Built client chat frame URL.', { | ||
| streamId: streamId, | ||
| ecpOrigin: ecpOrigin, | ||
| mode: mode, | ||
| hasTheme: Boolean(theme), | ||
| url: url.toString(), | ||
| }); | ||
| return url.toString(); | ||
| } | ||
|
|
||
| function ensureFrame() { | ||
| if (frame) { | ||
| return frame; | ||
| } | ||
|
|
||
| frame = document.createElement('iframe'); | ||
| frame.setAttribute('title', 'Wealth Client Chat Frame'); | ||
| frame.setAttribute('style', 'width:100%;height:100%;border:0;'); | ||
| frame.addEventListener('load', function () { | ||
| debug('Client chat iframe load event fired.', { | ||
| activeStreamId: activeStreamId, | ||
| src: frame ? frame.src : null, | ||
| }); | ||
| }); | ||
| document.getElementById(CONTAINER_ID).replaceChildren(frame); | ||
| debug('Created client chat iframe container.', { containerId: CONTAINER_ID }); | ||
| return frame; | ||
| } | ||
|
|
||
| function openStream(streamId) { | ||
| if (!streamId) { | ||
| debug('Skipped openStream because stream id was empty.'); | ||
| return; | ||
| } | ||
|
|
||
| if (activeStreamId === streamId && frame) { | ||
| debug('Skipped iframe navigation because stream is already active.', { | ||
| streamId: streamId, | ||
| }); | ||
| if (isClientReady) { | ||
| postToParent('ready', { streamId: activeStreamId }); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| activeStreamId = streamId; | ||
| isClientReady = false; | ||
| debug('Opening client chat stream in iframe.', { | ||
| streamId: streamId, | ||
| previousStreamId: frame ? frame.getAttribute('data-stream-id') : null, | ||
| }); | ||
| ensureFrame().setAttribute('data-stream-id', streamId); | ||
| ensureFrame().src = buildFrameUrl(streamId); | ||
| } | ||
|
|
||
| function postEcpMessage(eventType, payload) { | ||
| const targetFrame = ensureFrame(); | ||
| if (!targetFrame.contentWindow) { | ||
| throw new Error('Client chat iframe window is not available.'); | ||
| } | ||
|
|
||
| debug('Posting message to embedded ECP frame.', { | ||
| eventType: eventType, | ||
| payload: payload, | ||
| targetOrigin: podUrl, | ||
| }); | ||
| targetFrame.contentWindow.postMessage({ eventType: eventType, payload: payload }, podUrl); | ||
| } | ||
|
|
||
| function sendMessageToStream(requestId, documentName, messagePayload, streamId) { | ||
| const targetStreamId = streamId || activeStreamId; | ||
| if (!targetStreamId) { | ||
| throw new Error('Client chat stream id is missing.'); | ||
| } | ||
|
|
||
| if (!isClientReady) { | ||
| throw new Error('Client chat is not ready yet.'); | ||
| } | ||
|
|
||
| const actionRequestId = 'send-message-' + ++actionId; | ||
| pendingSendActions[actionRequestId] = { | ||
| requestId: requestId, | ||
| documentName: documentName, | ||
| streamId: targetStreamId, | ||
| }; | ||
|
|
||
| debug('Sending document share request to embedded client chat.', { | ||
| requestId: requestId, | ||
| actionRequestId: actionRequestId, | ||
| documentName: documentName, | ||
| streamId: targetStreamId, | ||
| messagePayload: messagePayload, | ||
| }); | ||
|
|
||
| postEcpMessage('sdk-action', { | ||
| name: 'send-message', | ||
| id: actionRequestId, | ||
| params: { | ||
| message: messagePayload, | ||
| options: { | ||
| mode: 'blast', | ||
| streamIds: [targetStreamId], | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| window.addEventListener('message', function (event) { | ||
| if (event.origin === podUrl) { | ||
| const ecpData = event.data || {}; | ||
| debug('Received embedded ECP event.', ecpData); | ||
|
|
||
| if (ecpData.eventType === 'clientReady') { | ||
| isClientReady = true; | ||
| postToParent('ready', { streamId: activeStreamId }); | ||
| return; | ||
| } | ||
|
|
||
| if (ecpData.eventType === 'sdk-resolve') { | ||
| const payload = ecpData.payload || {}; | ||
| const pendingAction = pendingSendActions[payload.id]; | ||
| if (!pendingAction) { | ||
| return; | ||
| } | ||
|
|
||
| delete pendingSendActions[payload.id]; | ||
|
|
||
| if (payload.data && payload.data.error) { | ||
| error('Embedded ECP send-message failed.', { | ||
| actionRequestId: payload.id, | ||
| requestId: pendingAction.requestId, | ||
| documentName: pendingAction.documentName, | ||
| error: payload.data.error, | ||
| }); | ||
| postToParent('share-error', { | ||
| requestId: pendingAction.requestId, | ||
| documentName: pendingAction.documentName, | ||
| message: payload.data.error.message || 'Unable to share document to client chat.', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| debug('Embedded ECP send-message succeeded.', { | ||
| actionRequestId: payload.id, | ||
| requestId: pendingAction.requestId, | ||
| documentName: pendingAction.documentName, | ||
| streamId: pendingAction.streamId, | ||
| }); | ||
| postToParent('share-success', { | ||
| requestId: pendingAction.requestId, | ||
| documentName: pendingAction.documentName, | ||
| streamId: pendingAction.streamId, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (event.origin !== window.location.origin) { | ||
| return; | ||
| } | ||
|
|
||
| const data = event.data || {}; | ||
| if (data.source !== 'wealth-client-chat-parent') { | ||
| return; | ||
| } | ||
|
|
||
| debug('Received parent message.', { | ||
| type: data.type, | ||
| payload: data.payload || {}, | ||
| }); | ||
|
|
||
| if (data.type === 'open-stream') { | ||
| openStream(data.payload && data.payload.streamId); | ||
| return; | ||
| } | ||
|
|
||
| if (data.type === 'send-message') { | ||
| try { | ||
| sendMessageToStream( | ||
| data.payload && data.payload.requestId, | ||
| data.payload && data.payload.documentName, | ||
| data.payload && data.payload.message, | ||
| data.payload && data.payload.streamId, | ||
| ); | ||
| } catch (sendError) { | ||
| const message = sendError instanceof Error ? sendError.message : 'Unable to share document to client chat.'; | ||
| warn('Unable to forward share request to embedded client chat.', { | ||
| error: message, | ||
| payload: data.payload || {}, | ||
| }); | ||
| postToParent('share-error', { | ||
| requestId: data.payload && data.payload.requestId, | ||
| documentName: data.payload && data.payload.documentName, | ||
| message: message, | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| debug('Initializing wealth client chat host.', { | ||
| ecpOrigin: ecpOrigin, | ||
| initialStreamId: initialStreamId, | ||
| partnerId: partnerId || DEFAULT_PARTNER_ID, | ||
| }); | ||
| openStream(initialStreamId); | ||
| })(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guard
if (!document.getElementById('symphony-ecm')) return;runs while the script is executing in<head>, before the<div id="symphony-ecm">in<body>has been parsed. As a result, the error listener is never registered and cross-origin "Script error." events will not be suppressed as intended. Consider removing the DOM check entirely, or deferring this setup until afterDOMContentLoaded(or by placing the script after thesymphony-ecmelement).