diff --git a/client/src/api/index.js b/client/src/api/index.js index dcf7ca7..8f9a889 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -88,5 +88,9 @@ export const api = Object.freeze({ return error.response; } }, + getWorkflowInstance: async (workflowId, instanceId) => { + const response = await instance.get(`/workflows/${workflowId}/instances/${instanceId}`); + return response; + }, }, }); diff --git a/client/src/assets/text.json b/client/src/assets/text.json index 1aab2ac..ecf0a07 100644 --- a/client/src/assets/text.json +++ b/client/src/assets/text.json @@ -93,7 +93,8 @@ "triggerNewWorkflow": "Trigger new workflow ->", "getStarted": "Get started", "moreInfo": "More info", - "backHome": "← Back to workflows" + "backHome": "← Back to workflows", + "done": "Done" }, "links": { "github": "https://github.com/docusign/sample-app-workflows-node", diff --git a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx index 0fd7c83..ab38842 100644 --- a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx +++ b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx @@ -22,7 +22,7 @@ const WorkflowTriggerResult = ({ workflowInstanceUrl }) => {
- See Embedded workflow instance recommendations and restrictions for more information. + See Embedded workflow instance recommendations and restrictions for more information.
{textContent.buttons.continue} diff --git a/client/src/components/TriggerForm/TriggerForm.jsx b/client/src/components/TriggerForm/TriggerForm.jsx index 6ee63be..dac2811 100644 --- a/client/src/components/TriggerForm/TriggerForm.jsx +++ b/client/src/components/TriggerForm/TriggerForm.jsx @@ -103,27 +103,17 @@ const TriggerForm = ({ workflowId, templateType }) => { } const { data: triggeredWorkflow } = await api.workflows.triggerWorkflow(workflowId, templateType, body); - setWorkflowInstanceUrl(triggeredWorkflow.instanceUrl); - - if (triggeredWorkflow.instanceUrl !== undefined) { - navigate(`${ROUTE.TRIGGERFORM}/${workflowId}?type=${templateType}&triggerUrl=${encodeURIComponent(triggeredWorkflow.instanceUrl)}`) - } if (triggeredWorkflow === WorkflowTriggerResponse.TRIGGER_ISSUE) { - // Update workflowDefinitions. "...workflow" creates new workflow-object to avoid mutation in redux - const updatedWorkflowDefinitions = workflows.map(w => { - if (w.id !== workflowId) return { ...w }; - - return { - ...w, - instanceId: triggeredWorkflow.instanceId, - isTriggered: true, - }; - }); - - dispatch(updateWorkflowDefinitions(updatedWorkflowDefinitions)); setDataSending(false); dispatch(openPopupWindow()); + return; + } + + setWorkflowInstanceUrl(triggeredWorkflow.instanceUrl); + + if (triggeredWorkflow.instanceUrl !== undefined) { + navigate(`${ROUTE.TRIGGERFORM}/${workflowId}?type=${templateType}&triggerUrl=${encodeURIComponent(triggeredWorkflow.instanceUrl)}`) } }; diff --git a/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx b/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx index bc28658..1138169 100644 --- a/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx +++ b/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx @@ -1,4 +1,5 @@ -import { useParams, useLocation } from 'react-router-dom'; +import { useState, useRef, useEffect, useCallback } from 'react'; +import { useParams, useLocation, useNavigate } from 'react-router-dom'; import styles from './TriggerWorkflowForm.module.css'; import Header from '../../components/Header/Header.jsx'; import Footer from '../../components/Footer/Footer.jsx'; @@ -8,32 +9,107 @@ import WorkflowDescription from '../../components/WorkflowDescription/WorkflowDe import TriggerBehindTheScenes from '../../components/WorkflowDescription/BehindTheScenes/TriggerBehindTheScenes.jsx'; import TriggerForm from '../../components/TriggerForm/TriggerForm.jsx'; import { ROUTE } from '../../constants.js'; +import { buttons } from '../../assets/text.json'; const TriggerWorkflowForm = () => { const { workflowId } = useParams(); + const navigate = useNavigate(); + const [embeddingFailed, setEmbeddingFailed] = useState(false); + const hasOpenedNewTab = useRef(false); + const childWindowRef = useRef(null); const location = useLocation(); const searchParams = new URLSearchParams(location.search); const type = searchParams.get('type'); const triggerUrl = searchParams.get('triggerUrl'); - const triggerUrlPattern = /^https:\/\/(?!.*javascript)[^()]+$/i; - -function isValidTriggerUrl(url) { - try { - const decoded = decodeURIComponent(url); - const parsedUrl = new URL(decoded); - // Only allow https and the exact hostname - return ( - parsedUrl.protocol === 'https:' && - parsedUrl.hostname === 'apps-d.docusign.com' - ); - } catch { - return false; + function isValidTriggerUrl(url) { + try { + const decoded = decodeURIComponent(url); + const parsedUrl = new URL(decoded); + return ( + parsedUrl.protocol === 'https:' && + parsedUrl.hostname === 'apps-d.docusign.com' + ); + } catch { + return false; + } } -} - + + const handleIframeLoad = useCallback((e) => { + try { + const href = e.target.contentWindow.location.href; + if (href === 'about:blank') { + setEmbeddingFailed(true); + } + } catch { + // Cross-origin SecurityError — handled by ReportingObserver for CSP blocks + } + }, []); + + // Detect CSP frame-ancestors violations via ReportingObserver + useEffect(() => { + if (!triggerUrl) return; + + let observer; + if (typeof ReportingObserver !== 'undefined') { + observer = new ReportingObserver((reports) => { + const blocked = reports.some( + (report) => report.body && report.body.effectiveDirective === 'frame-ancestors' + ); + if (blocked) { + setEmbeddingFailed(true); + } + }, { types: ['csp-violation'], buffered: false }); + observer.observe(); + } + + return () => { + if (observer) observer.disconnect(); + }; + }, [triggerUrl]); + + // Open new tab when embedding fails + useEffect(() => { + if (embeddingFailed && triggerUrl && !hasOpenedNewTab.current) { + hasOpenedNewTab.current = true; + childWindowRef.current = window.open(triggerUrl, '_blank'); + } + }, [embeddingFailed, triggerUrl]); + if (triggerUrl !== null && isValidTriggerUrl(triggerUrl)) { + // Embedding failed — workflow opened in new tab + if (embeddingFailed) { + return ( +This workflow cannot be embedded and has been opened in a new tab.
+Complete your tasks in the new tab, then click Done to return.
+ + Open workflow in new tab + + +