Skip to content

Commit

Permalink
Docu sign (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
michal-grzelak authored Jul 31, 2023
1 parent 863d3c6 commit d7c92ff
Show file tree
Hide file tree
Showing 15 changed files with 467 additions and 69 deletions.
7 changes: 7 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
26 changes: 26 additions & 0 deletions src/components/Extension/DocuSignExtension/DocuSignExtension.tsx
Original file line number Diff line number Diff line change
@@ -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<DocuSignExtensionProps> = ({
activityDetails,
}) => {
const { t } = useTranslation()

switch (activityDetails.plugin_action_key) {
case ActionKey.EMBEDDED_SIGNING:
return <EmbeddedSigningAction activityDetails={activityDetails} />
default:
return <ErrorPage title={t('activities.activity_not_supported')} />
}
}

DocuSignExtension.displayName = 'DropboxSignExtension'
Original file line number Diff line number Diff line change
@@ -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<EmbeddedSigningActionActionProps> = ({
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<DocuSignEvent | undefined>(undefined)
const isFinished = !!event
const isIframe = !!iframeEvent

const { signUrl } = useMemo(
() => mapActionFieldsToObject<EmbeddedSigningFields>(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&#x3D;ABC&amp;token&#x3D;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 <IFrameMessager iframeEvent={iframeEvent} setEvent={setEvent} />
}

// this window is extension content -> render messager and extension
return (
<>
<IFrameMessager iframeEvent={iframeEvent} setEvent={setEvent} />

<div
// if iframe is loaded -> fill screen
className={`${classes.wrapper} ${
isIframeLoaded ? classes['flex-full'] : ''
}`}
>
{isFinished ? (
// signing process is finished -> display messsage
<FinishedMessage event={event} finishSigning={finishSigning} />
) : (
// signing process incomplete (or user refreshed page) -> display signing process
<SigningProcess
signUrl={decodedSignUrl}
isIframeLoaded={isIframeLoaded}
setIsIframeLoaded={setIsIframeLoaded}
/>
)}
</div>
</>
)
}

EmbeddedSigningAction.displayName = 'EmbeddedSigningAction'
Original file line number Diff line number Diff line change
@@ -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<FinishedMessage> = ({
event,
finishSigning,
}) => {
const { t } = useTranslation()

const finishAndFailSigning = () => {
finishSigning({ signed: false })
}

switch (event) {
case DocuSignEvent.SIGNING_COMPLETE:
return (
<LoadingPage title={t('activities.docu_sign.finished_sign_document')} />
)
case DocuSignEvent.TTL_EXPIRED:
return (
<>
<span>
<h2>{t('activities.docu_sign.expired_sign_document')}</h2>
<Button onClick={finishAndFailSigning}>
{t('activities.cta_done')}
</Button>
</span>
</>
)
default:
return (
<>
<span>
<h2>{t('activities.docu_sign.failed_sign_document')}</h2>
<Button onClick={finishAndFailSigning}>
{t('activities.cta_done')}
</Button>
</span>
</>
)
}
}

FinishedMessage.displayName = 'FinishedMessage'
Original file line number Diff line number Diff line change
@@ -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<IFrameMessager> = ({
iframeEvent,
setEvent,
}) => {
const isIframe = !!iframeEvent

const handleDocuSignEvent = useCallback(
(event: MessageEvent<DocuSignMessage>) => {
// 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'
Original file line number Diff line number Diff line change
@@ -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<SigningProcess> = ({
signUrl,
isIframeLoaded,
setIsIframeLoaded,
}) => {
const { t } = useTranslation()

const [isSignProcess, setIsSignProcess] = useState(false)

const beginSigning = () => {
setIsSignProcess(true)
}

if (isSignProcess) {
return (
<>
{!isIframeLoaded && (
<LoadingPage
title={t('activities.docu_sign.loading_sign_document')}
/>
)}
<iframe
className={
isIframeLoaded
? classes['iframe-loaded']
: classes['iframe-loading']
}
src={signUrl}
onLoad={() => {
setIsIframeLoaded(true)
}}
/>
</>
)
}

return (
<span>
<Button onClick={beginSigning}>
{t('activities.docu_sign.cta_sign_document')}
</Button>
</span>
)
}

SigningProcess.displayName = 'SigningProcess'
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.wrapper {
display: flex;
justify-content: center;
text-align: center;
padding: 12px 0 0 0;
}

.flex-full {
flex: 1 0;
}

.wrapper > iframe {
border: 0;
}

.iframe-loaded {
width: 80%;
height: 80%;
}

.iframe-loading {
width: 0;
height: 0;
visibility: hidden;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useCallback } from 'react'
import { DataPoints, useCompleteExtensionActivity } from '../../../types'

export const useCompleteEmbeddedSigningAction = () => {
const { isSubmitting, onSubmit: _onSubmit } = useCompleteExtensionActivity()

const onSubmit = useCallback(
async (activityId: string, { signed }: { signed: boolean }) => {
const dataPoints: DataPoints = [{ key: 'signed', value: String(signed) }]

return _onSubmit(activityId, dataPoints)
},
[_onSubmit]
)

return {
isSubmitting,
onSubmit,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EmbeddedSigningAction } from './EmbeddedSigningAction'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EmbeddedSigningAction } from './EmbeddedSigningAction'
1 change: 1 addition & 0 deletions src/components/Extension/DocuSignExtension/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DocuSignExtension } from './DocuSignExtension'
Loading

0 comments on commit d7c92ff

Please sign in to comment.