diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 8678d6bb..0f232641 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -56,6 +56,13 @@ "file_count_zero": "You have not uploaded any files yet.", "file_count_one": "You have uploaded 1 file.", "file_count_other": "You have uploaded {{count}} files." + }, + "docu_sign": { + "cta_sign_document": "Sign document", + "loading_sign_document": "Loading the document", + "finished_sign_document": "Wrapping up", + "expired_sign_document": "Signing link has expired.", + "failed_sign_document": "Signing link has failed." } }, "preview": { diff --git a/src/components/Extension/DocuSignExtension/DocuSignExtension.tsx b/src/components/Extension/DocuSignExtension/DocuSignExtension.tsx new file mode 100644 index 00000000..0460529b --- /dev/null +++ b/src/components/Extension/DocuSignExtension/DocuSignExtension.tsx @@ -0,0 +1,26 @@ +import React, { FC } from 'react' +import { useTranslation } from 'next-i18next' +import { ErrorPage } from '../../ErrorPage' +import { EmbeddedSigningAction } from './actions' +import { ActionKey } from './types' +import type { Activity, ExtensionActivityRecord } from '../types' + +interface DocuSignExtensionProps { + activity: Activity + activityDetails: ExtensionActivityRecord +} + +export const DocuSignExtension: FC = ({ + activityDetails, +}) => { + const { t } = useTranslation() + + switch (activityDetails.plugin_action_key) { + case ActionKey.EMBEDDED_SIGNING: + return + default: + return + } +} + +DocuSignExtension.displayName = 'DropboxSignExtension' diff --git a/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/EmbeddedSigningAction.tsx b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/EmbeddedSigningAction.tsx new file mode 100644 index 00000000..a2887ba6 --- /dev/null +++ b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/EmbeddedSigningAction.tsx @@ -0,0 +1,94 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { parse } from 'query-string' +import he from 'he' +import { mapActionFieldsToObject } from '../../../utils' +import { DocuSignEvent, type EmbeddedSigningFields } from '../../types' +import type { ExtensionActivityRecord } from '../../../types' +import { useCompleteEmbeddedSigningAction } from './hooks/useCompleteEmbeddedSigningAction' +import { IFrameMessager } from './IFrameMessager' +import { SigningProcess } from './SigningProcess' +import { FinishedMessage } from './FinishedMessage' +import classes from './embeddedSigning.module.css' + +interface EmbeddedSigningActionActionProps { + activityDetails: ExtensionActivityRecord +} + +export const EmbeddedSigningAction: FC = ({ + activityDetails, +}) => { + const { activity_id, fields } = activityDetails + const { onSubmit } = useCompleteEmbeddedSigningAction() + // visible in IFrame only + const { event: iframeEvent } = + (parse(location.search) as { event?: DocuSignEvent }) ?? {} + const [isIframeLoaded, setIsIframeLoaded] = useState(false) + const [event, setEvent] = useState(undefined) + const isFinished = !!event + const isIframe = !!iframeEvent + + const { signUrl } = useMemo( + () => mapActionFieldsToObject(fields), + [fields] + ) + + /** + * This is needed because Orchestration seems to encode string action field values + * - Data point value: https://url.com/?signature_id=ABC&token=DEF + * - Action field value https://url.com/?signature_id=ABC&token=DEF + * + * We need the decoded action field value and he library helps us to force decode + * the url to a valid one. + */ + const decodedSignUrl = he.decode(signUrl) + + const finishSigning = useCallback( + ({ signed }: { signed: boolean }) => { + onSubmit(activity_id, { signed }) + }, + [activity_id, onSubmit] + ) + + useEffect(() => { + if (isFinished) { + setIsIframeLoaded(false) + + if (event === DocuSignEvent.SIGNING_COMPLETE) { + finishSigning({ signed: true }) + } + } + }, [event, finishSigning, isFinished]) + + // this window is iframe content -> render only messager + if (isIframe) { + return + } + + // this window is extension content -> render messager and extension + return ( + <> + + +
fill screen + className={`${classes.wrapper} ${ + isIframeLoaded ? classes['flex-full'] : '' + }`} + > + {isFinished ? ( + // signing process is finished -> display messsage + + ) : ( + // signing process incomplete (or user refreshed page) -> display signing process + + )} +
+ + ) +} + +EmbeddedSigningAction.displayName = 'EmbeddedSigningAction' diff --git a/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/FinishedMessage.tsx b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/FinishedMessage.tsx new file mode 100644 index 00000000..09990a95 --- /dev/null +++ b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/FinishedMessage.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react' +import { useTranslation } from 'next-i18next' +import { Button } from '@awell_health/ui-library' +import { DocuSignEvent } from '../../types' +import { LoadingPage } from '../../../../LoadingPage' + +interface FinishedMessage { + event: DocuSignEvent + finishSigning: (args: { signed: boolean }) => void +} + +export const FinishedMessage: FC = ({ + event, + finishSigning, +}) => { + const { t } = useTranslation() + + const finishAndFailSigning = () => { + finishSigning({ signed: false }) + } + + switch (event) { + case DocuSignEvent.SIGNING_COMPLETE: + return ( + + ) + case DocuSignEvent.TTL_EXPIRED: + return ( + <> + +

{t('activities.docu_sign.expired_sign_document')}

+ +
+ + ) + default: + return ( + <> + +

{t('activities.docu_sign.failed_sign_document')}

+ +
+ + ) + } +} + +FinishedMessage.displayName = 'FinishedMessage' diff --git a/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/IFrameMessager.tsx b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/IFrameMessager.tsx new file mode 100644 index 00000000..f680d73b --- /dev/null +++ b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/IFrameMessager.tsx @@ -0,0 +1,51 @@ +import React, { FC, useCallback, useEffect } from 'react' +import { DocuSignEvent, DocuSignMessage, WindowEventType } from '../../types' + +interface IFrameMessager { + iframeEvent?: DocuSignEvent + setEvent: (event: DocuSignEvent | undefined) => void +} + +export const IFrameMessager: FC = ({ + iframeEvent, + setEvent, +}) => { + const isIframe = !!iframeEvent + + const handleDocuSignEvent = useCallback( + (event: MessageEvent) => { + // validate domain + if (event.origin !== location.origin) { + return + } + + switch (event.data?.type) { + case WindowEventType.DOCU_SIGN_SET_EVENT: + setEvent(event.data.event) + break + default: + break + } + }, + [setEvent] + ) + + useEffect(() => { + if (isIframe) { + window.top?.postMessage( + { event: iframeEvent, type: WindowEventType.DOCU_SIGN_SET_EVENT }, + location.origin + ) + } else { + window.addEventListener('message', handleDocuSignEvent, false) + + return () => { + window.removeEventListener('message', handleDocuSignEvent, false) + } + } + }, [handleDocuSignEvent, iframeEvent, isIframe]) + + return <> +} + +IFrameMessager.displayName = 'IFrameMessager' diff --git a/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/SigningProcess.tsx b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/SigningProcess.tsx new file mode 100644 index 00000000..65f4a2ce --- /dev/null +++ b/src/components/Extension/DocuSignExtension/actions/EmbeddedSigningAction/SigningProcess.tsx @@ -0,0 +1,58 @@ +import React, { FC, useState } from 'react' +import { useTranslation } from 'next-i18next' +import { Button } from '@awell_health/ui-library' +import classes from './embeddedSigning.module.css' +import { LoadingPage } from '../../../../LoadingPage' + +interface SigningProcess { + signUrl: string + isIframeLoaded: boolean + setIsIframeLoaded: (isLoaded: boolean) => void +} + +export const SigningProcess: FC = ({ + signUrl, + isIframeLoaded, + setIsIframeLoaded, +}) => { + const { t } = useTranslation() + + const [isSignProcess, setIsSignProcess] = useState(false) + + const beginSigning = () => { + setIsSignProcess(true) + } + + if (isSignProcess) { + return ( + <> + {!isIframeLoaded && ( + + )} +