Skip to content
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

Docu sign #142

Merged
merged 8 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
michal-grzelak marked this conversation as resolved.
Show resolved Hide resolved
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
Loading