Skip to content

Commit

Permalink
Identity check with DOB (#261)
Browse files Browse the repository at this point in the history
* feat(): identity check

* chore(): pass data point

* chore(): translations and loading state

* chore(): more verbose fn name

* feat(identity-verify): move verification to backend

---------

Co-authored-by: JB <[email protected]>
  • Loading branch information
nckhell and bejoinka authored Sep 11, 2024
1 parent c83047c commit e61d65e
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './jwt-feature'
8 changes: 8 additions & 0 deletions lib/jwt-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable no-unused-vars */
enum JwtFeature {
HostedActivitiesLink = 'hosted-activities-link',
HostedPathwayLink = 'hosted-pathway-link',
IdentityVerification = 'identity-verification',
}

export { JwtFeature }
121 changes: 121 additions & 0 deletions pages/api/identity/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import jwt from 'jsonwebtoken'
import { environment } from '../../../types'
import { z, ZodIssue } from 'zod'
import { JwtFeature } from '../../../lib'

export const BodySchema = z.object({
dob: z.coerce.date(),
activity_id: z.string(),
pathway_id: z.string(),
})

type Response =
| {
success: boolean
}
| {
error: {
message: string
errors: ZodIssue[]
}
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Response>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST')
return res.status(405).end('Method Not Allowed')
}

const bodyValidation = BodySchema.safeParse(req.body)

if (!bodyValidation.success) {
const { errors } = bodyValidation.error

return res.status(400).json({
error: { message: 'Invalid request', errors },
})
}

const { activity_id, pathway_id, dob } = bodyValidation.data

// TODO JB
const token = jwt.sign(
{
username: environment.apiGatewayConsumerName,
feature: JwtFeature.IdentityVerification,
},
environment.jwtAuthSecret,
{
issuer: environment.jwtAuthKey,
subject: activity_id,
}
)

// LATER: we have an "i don't know my DOB" option then we can complete with failure... etc.
const input = { pathway_id, dob }
// TODO JB
const response = await fetch(environment.orchestrationApiUrl, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
mutation Verify_identity($input: VerifyIdentityInput!) {
verify_identity(input: $input) {
code
success
is_verified
}
}
`,
variables: { input },
}),
})

const resp = await response.json()
console.log(resp)
const { data, errors } = resp
const { is_verified } = data?.verify_identity

if (is_verified) {
await fetch(environment.orchestrationApiUrl, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
mutation CompleteExtensionActivity($input: CompleteExtensionActivityInput!) {
completeExtensionActivity(input: $input) {
code
success
}
}
`,
variables: {
input: {
activity_id,
data_points: [
{
key: 'success',
value: 'true',
},
],
},
},
}),
})
}

res.status(200).json({
success: is_verified,
...(errors && errors.length > 0 && { error: errors }),
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
StartHostedActivitySessionPayload,
} from '../../../types'
import { isNil } from 'lodash'
import { JwtFeature } from '../../../lib'

type Data =
| StartHostedActivitySessionPayload
Expand All @@ -23,7 +24,7 @@ export default async function handler(
const token = jwt.sign(
{
username: environment.apiGatewayConsumerName,
feature: 'hosted-activities-link',
feature: JwtFeature.HostedActivitiesLink,
},
environment.jwtAuthSecret,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import jwt from 'jsonwebtoken'
import { environment } from '../../../types'
import { isNil } from 'lodash'
import { JwtFeature } from '../../../lib'

export type StartHostedCareflowSessionParams = {
hostedPagesLinkId: string
Expand Down Expand Up @@ -39,7 +40,7 @@ export default async function handler(
const token = jwt.sign(
{
username: environment.apiGatewayConsumerName,
feature: 'hosted-pathway-link',
feature: JwtFeature.HostedPathwayLink,
},
environment.jwtAuthSecret,
{
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"medication_name": "Name",
"submit_button": "Submit"
},
"identity_verification": {
"default_label": "Enter your date of birth to verify your identity",
"cta": "Confirm"
},
"cta_done": "Done",
"docu_sign": {
"cta_sign_document": "Sign document",
Expand Down
3 changes: 3 additions & 0 deletions src/components/Extension/Extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Redirect as PrivateRedirect,
} from './PrivateExtensions/actions'
import { StripeExtension } from './StripeExtension'
import { IdentityVerification } from './IdentityVerification'

interface ExtensionProps {
activity: Activity
Expand Down Expand Up @@ -82,6 +83,8 @@ export const Extension: FC<ExtensionProps> = ({ activity }) => {
return <CollectDataExtension activityDetails={extensionActivityDetails} />
case ExtensionKey.STRIPE:
return <StripeExtension activityDetails={extensionActivityDetails} />
case ExtensionKey.IDENTITY_VERIFICATION:
return <IdentityVerification activityDetails={extensionActivityDetails} />
case ExtensionKey.EXPERIMENTAL:
return (
<ExperimentalExtension activityDetails={extensionActivityDetails} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FC } from 'react'
import { useTranslation } from 'next-i18next'

import { ErrorPage } from '../../ErrorPage'
import { DobCheck } from './actions'

import { ActionKey } from './types'
import type { ExtensionActivityRecord } from '../types'

interface IdentityVerificationExtensionProps {
activityDetails: ExtensionActivityRecord
}

export const IdentityVerification: FC<IdentityVerificationExtensionProps> = ({
activityDetails,
}) => {
const { t } = useTranslation()

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

IdentityVerification.displayName = 'IdentityVerification'
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.container {
max-width: 95%;
margin: 0 auto;
}

@media (min-width: 640px) {
.container {
max-width: 850px;
}
}

.label {
text-align: left;
}

.inputWrapper {
max-width: 550px;
margin: 0 auto;
}

.submitButton {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { FC, useCallback, useMemo, useState } from 'react'
import classes from './DobCheck.module.css'
import activityClasses from '../../../../../../styles/ActivityLayout.module.css'

import type { ExtensionActivityRecord } from '../../../types'
import { useDobCheck } from './hooks/useDobCheck'
import {
Button,
CircularSpinner,
HostedPageFooter,
InputField,
} from '@awell-health/ui-library'
import { isEmpty } from 'lodash'
import { mapActionFieldsToObject } from '../../../utils'
import { DobCheckActionFields } from './types'
import { useTranslation } from 'next-i18next'

interface DobCheckProps {
activityDetails: ExtensionActivityRecord
}

export const DobCheck: FC<DobCheckProps> = ({ activityDetails }) => {
const { t } = useTranslation()

const [dobValue, setDobValue] = useState('')
const [loading, setLoading] = useState(false)
const { activity_id, fields, pathway_id } = activityDetails

const { onSubmit } = useDobCheck()

const { label } = useMemo(
() => mapActionFieldsToObject<DobCheckActionFields>(fields),
[fields]
)

const handleActivityCompletion = useCallback(() => {
onSubmit({
activityId: activity_id,
})
}, [activity_id, onSubmit])

const handleDobCheck = useCallback(async () => {
if (isEmpty(dobValue)) {
// Prettify this later
alert('Please enter a date of birth')
return
}

try {
setLoading(true)
const response = await fetch('/api/identity/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ dob: dobValue, pathway_id, activity_id }),
})

setLoading(false)

if (!response.ok) {
throw new Error('Failed to check dob')
}

const jsonRes = await response.json()

if (!jsonRes?.success) {
// Prettify this later
alert('No match')
return
}

handleActivityCompletion()
} catch (error) {
console.error('Error checking dob:', error)
throw error
}
}, [dobValue, handleActivityCompletion])

return (
<>
<main
id="ahp_main_content_with_scroll_hint"
className={activityClasses.main_content}
>
<div
className={`${classes.container} ${classes.groupMedsListContainer}`}
>
<div className={classes.inputWrapper}>
{/* We should prettify the loading state */}
{loading ? (
<CircularSpinner size="sm" />
) : (
<InputField
id="name"
label={
label ?? t('activities.identity_verification.default_label')
}
type="date"
value={dobValue}
onChange={(e) => setDobValue(e.target.value)}
/>
)}
</div>
</div>
</main>
<HostedPageFooter showScrollHint={false}>
<div
className={`${activityClasses.button_wrapper} ${classes.container}`}
>
<Button variant="primary" onClick={handleDobCheck} disabled={loading}>
{t('activities.identity_verification.cta')}
</Button>
</div>
</HostedPageFooter>
</>
)
}

DobCheck.displayName = 'DobCheck'
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useCallback } from 'react'
import { useCompleteExtensionActivity } from '../../../types'

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

const onSubmit = useCallback(
async ({ activityId }: { activityId: string }) => {
return _onSubmit(activityId, [])
},
[_onSubmit]
)

return {
isSubmitting,
onSubmit,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DobCheck } from './DobCheck'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type DobCheckActionFields = {
label?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DobCheck } from './DobCheck'
1 change: 1 addition & 0 deletions src/components/Extension/IdentityVerification/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { IdentityVerification } from './IdentityVerification'
Loading

0 comments on commit e61d65e

Please sign in to comment.