onfido-sdk-ui
Version:
JavaScript SDK view layer for Onfido identity verification
381 lines (334 loc) • 10 kB
text/typescript
import { hmac256, mimeType } from './blob'
import { parseJwt } from './jwt'
import { performHttpReq, HttpRequestParams } from './http'
import { forEach } from './object'
import { trackException } from '../../Tracker'
import type {
ImageQualityValidationPayload,
ApiRawError,
UploadFileResponse,
DocumentImageResponse,
ChallengeData,
FaceVideoResponse,
VideoChallengeResponse,
SnapshotResponse,
UploadBinaryMediaReponse,
CreateV4DocumentResponse,
SuccessCallback,
ErrorCallback,
} from '~types/api'
import type { DocumentSides, SdkMetadata, FilePayload } from '~types/commons'
import type { SupportedLanguages } from '~types/locales'
import type { LegacyTrackedEventNames } from '~types/tracker'
import type { DocumentTypes, PoaTypes } from '~types/steps'
type UploadPayload = {
filename?: string
sdkMetadata: SdkMetadata
}
type UploadDocumentPayload = {
file: Blob
issuing_country?: string
side?: DocumentSides
type?: DocumentTypes | PoaTypes | 'unknown'
validations?: ImageQualityValidationPayload
} & UploadPayload
type UploadVideoPayload = {
blob: Blob
challengeData?: ChallengeData
language?: SupportedLanguages
} & UploadPayload
type UploadSnapshotPayload = {
file: Blob | FilePayload
}
type UploadLivePhotoPayload = {
file: Blob | FilePayload
snapshot_uuids?: string
} & UploadPayload
type SelfiePayload = { blob: Blob } & UploadPayload
type SubmitPayload = Omit<UploadPayload, 'sdkMetadata'> & {
file?: Blob | FilePayload
sdk_metadata?: string
sdk_source?: string
sdk_validations?: string
sdk_version?: string
}
type SubmitLiveVideoPayload = {
challenge?: string
challenge_id?: string
challenge_switch_at?: number
languages?: string
} & SubmitPayload
export const formatError = (
{ response, status }: ApiRawError,
onError: ErrorCallback
): void => {
try {
onError({ status, response: JSON.parse(response) })
} catch {
onError({ status, response: {} })
}
}
export const uploadDocument = (
payload: UploadDocumentPayload,
url: string | undefined,
token: string | undefined,
onSuccess?: SuccessCallback<DocumentImageResponse>,
onError?: ErrorCallback
): Promise<DocumentImageResponse> => {
const { sdkMetadata, validations = {}, ...other } = payload
const data: SubmitPayload = {
...other,
sdk_metadata: JSON.stringify(sdkMetadata),
sdk_validations: JSON.stringify(validations),
}
const endpoint = `${url}/v3/documents`
return new Promise((resolve, reject) =>
sendFile(endpoint, data, token, onSuccess || resolve, onError || reject)
)
}
export const uploadFacePhoto = (
{ sdkMetadata, ...data }: UploadLivePhotoPayload,
url: string | undefined,
token: string | undefined,
onSuccess: SuccessCallback<UploadFileResponse>,
onError: ErrorCallback
): void => {
const endpoint = `${url}/v3/live_photos`
sendFile(
endpoint,
{ ...data, sdk_metadata: JSON.stringify(sdkMetadata) },
token,
onSuccess,
onError
)
}
export const uploadSnapshot = (
payload: UploadSnapshotPayload,
url: string | undefined,
token: string | undefined,
onSuccess: SuccessCallback<SnapshotResponse>,
onError: ErrorCallback
): void => {
const endpoint = `${url}/v3/snapshots`
sendFile(endpoint, payload, token, onSuccess, onError)
}
export const sendMultiframeSelfie = (
snapshot: FilePayload,
selfie: SelfiePayload,
token: string | undefined,
url: string | undefined,
onSuccess: SuccessCallback<UploadFileResponse>,
onError: ErrorCallback,
sendEvent: (
event: LegacyTrackedEventNames,
properties?: Record<string, unknown>
) => void
): void => {
const snapshotData: UploadSnapshotPayload = {
file: {
blob: snapshot.blob,
filename: snapshot.filename,
},
}
const { blob, filename = 'selfie', sdkMetadata } = selfie
new Promise<SnapshotResponse>((resolve, reject) => {
sendEvent('Starting snapshot upload')
uploadSnapshot(snapshotData, url, token, resolve, reject)
})
.then((res) => {
sendEvent('Snapshot upload completed')
sendEvent('Starting live photo upload')
const snapshot_uuids = JSON.stringify([res.uuid])
uploadFacePhoto(
{ file: { blob, filename }, sdkMetadata, snapshot_uuids },
url,
token,
onSuccess,
onError
)
})
.catch((err) => onError(err))
}
export const uploadFaceVideo = (
{ challengeData, blob, language, sdkMetadata }: UploadVideoPayload,
url: string | undefined,
token: string | undefined,
onSuccess?: SuccessCallback<FaceVideoResponse>,
onError?: ErrorCallback
): Promise<FaceVideoResponse> => {
const {
challenges: challenge,
id: challenge_id,
switchSeconds: challenge_switch_at,
} = challengeData || {}
// NOTE: important for automation - language_code string must be
// either 2-letter ISO, i.e. "en", or BCP-47 IIRC format, i.e. "en-US".
const languageCodeForApi = language && language.split('_')[0]
const payload: SubmitLiveVideoPayload = {
file: blob,
languages: JSON.stringify([
{ source: 'sdk', language_code: languageCodeForApi },
]),
challenge: JSON.stringify(challenge),
challenge_id,
challenge_switch_at,
sdk_metadata: JSON.stringify(sdkMetadata),
}
const endpoint = `${url}/v3/live_videos`
return new Promise((resolve, reject) =>
sendFile(endpoint, payload, token, onSuccess || resolve, onError || reject)
)
}
export const requestChallenges = (
url: string | undefined,
token: string | undefined,
onSuccess: SuccessCallback<VideoChallengeResponse>,
onError: ErrorCallback
): void => {
if (!url) {
throw new Error('onfido_api_url not provided')
}
if (!token) {
throw new Error('token not provided')
}
const options: HttpRequestParams = {
endpoint: `${url}/v3/live_video_challenge`,
contentType: 'application/json',
token: `Bearer ${token}`,
}
performHttpReq(options, onSuccess, (request) => formatError(request, onError))
}
/* v4 APIs */
export const uploadBinaryMedia = (
{ file, filename, sdkMetadata }: UploadDocumentPayload,
url: string | undefined,
token: string | undefined,
includeHmacAuth = false
): Promise<UploadBinaryMediaReponse> =>
new Promise((resolve, reject) => {
try {
const tokenData = parseJwt(token)
const formData = new FormData()
formData.append(
'media',
file,
filename || `document_capture.${mimeType(file)}`
)
formData.append('sdk_metadata', JSON.stringify(sdkMetadata))
if (!includeHmacAuth) {
const requestParams: HttpRequestParams = {
endpoint: `${url}/v4/binary_media`,
payload: formData,
token: `Bearer ${token}`,
}
performHttpReq(requestParams, resolve, (request) =>
formatError(request, reject)
)
return
}
file
.arrayBuffer()
.then((data) => hmac256(tokenData.uuid as string, data))
.then((hmac) => {
const requestParams: HttpRequestParams = {
endpoint: `${url}/v4/binary_media`,
headers: { 'X-Video-Auth': hmac },
payload: formData,
token: `Bearer ${token}`,
}
performHttpReq(requestParams, resolve, (request) =>
formatError(request, reject)
)
})
.catch(reject)
} catch (error) {
reject(error)
}
})
export const createV4Document = (
mediaIds: string[],
url: string | undefined,
token: string | undefined
): Promise<CreateV4DocumentResponse> =>
new Promise((resolve, reject) => {
try {
const requestParams: HttpRequestParams = {
contentType: 'application/json',
payload: JSON.stringify({
document_media: mediaIds.map((uuid) => ({ binary_media: { uuid } })),
}),
endpoint: `${url}/v4/documents`,
token: `Bearer ${token}`,
}
performHttpReq(requestParams, resolve, (request) =>
formatError(request, reject)
)
} catch (error) {
reject(error)
}
})
export const objectToFormData = (object: SubmitPayload): FormData => {
const formData = new FormData()
forEach(object, (value, fieldName) => {
if (
typeof value === 'string' ||
typeof value === 'number' || // challenge_switch_at is a numerical value & required for video automation
value instanceof Blob
) {
formData.append(fieldName, value)
} else if (typeof value === 'object') {
formData.append(fieldName, value.blob, value.filename)
}
})
return formData
}
const sendFile = <T>(
endpoint: string | undefined,
data: SubmitPayload,
token: string | undefined,
onSuccess: SuccessCallback<T>,
onError: ErrorCallback
) => {
if (!endpoint) {
throw new Error('onfido_api_url not provided')
}
if (!token) {
throw new Error('token not provided')
}
const payload: SubmitPayload = {
...data,
sdk_source: process.env.SDK_SOURCE,
sdk_version: process.env.SDK_VERSION,
}
const requestParams: HttpRequestParams = {
payload: objectToFormData(payload),
endpoint,
token: `Bearer ${token}`,
}
performHttpReq(requestParams, onSuccess, (request) => {
console.log('API error', request)
formatError(request, onError)
})
}
export const sendAnalytics = (
url: string | undefined,
payload: string
): void => {
const endpoint = `${url}/v3/analytics`
const request = new XMLHttpRequest()
request.open('POST', endpoint)
request.setRequestHeader('Content-Type', 'application/json')
request.onload = () => {
const isSuccessfulRequest = request.status === 200 || request.status === 201
if (!isSuccessfulRequest) {
trackException(
`analytics request error - status: ${request.status}, response: ${request.response}`
)
}
}
request.onerror = () =>
trackException(
`analytics request error - status: ${request.status}, response: ${request.response}`
)
request.send(payload)
}