From b59e06aee9881f584672fe99c10d81a6e98f9ed1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 11 Aug 2020 11:40:34 -0400 Subject: [PATCH 1/3] Convert propTypes to TS-based function JSDoc **Why:** To take advantage of TypeScript-based type checking. --- .eslintrc | 14 +-- .../components/acuant-capture-canvas.jsx | 52 ++++++---- .../document-capture/components/button.jsx | 46 +++++---- .../components/document-capture.jsx | 21 ++-- .../components/documents-step.jsx | 36 +++---- .../components/file-input.jsx | 53 ++++++----- .../components/form-steps.jsx | 38 ++++---- .../components/full-screen.jsx | 24 ++--- .../app/document-capture/components/image.jsx | 16 ++-- .../components/page-heading.jsx | 14 ++- .../components/selfie-step.jsx | 34 ++++--- .../components/submission-complete.jsx | 25 +++-- .../components/submission.jsx | 22 ++--- .../components/suspense-error-boundary.jsx | 20 ++-- .../app/document-capture/context/acuant.jsx | 95 ++++++++++++++----- package.json | 1 - 16 files changed, 300 insertions(+), 211 deletions(-) diff --git a/.eslintrc b/.eslintrc index 94f2eb8af88..3ce5f7478f5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,8 +30,8 @@ "implicit-arrow-linebreak": "off", "object-curly-newline": "off", "operator-linebreak": "off", - "react/prop-types": ["error", { "skipUndeclared": true }], - "react/jsx-one-expression-per-line": "off" + "react/jsx-one-expression-per-line": "off", + "react/prop-types": "off" }, "parserOptions": { "ecmaVersion": 6, @@ -50,13 +50,5 @@ "app/phone-internationalization", "app/i18n-dropdown" ] - }, - "overrides": [ - { - "files": ["spec/javascripts/**/*"], - "rules": { - "react/prop-types": "off" - } - } - ] + } } diff --git a/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx index fbecee013ee..6dec71a456a 100644 --- a/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/app/document-capture/components/acuant-capture-canvas.jsx @@ -1,5 +1,21 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; + +/** + * @typedef AcuantCameraUI + * + * @prop {(AcuantSuccessCallback,AcuantFailureCallback)=>void} start Start capture. + * @prop {()=>void} end End capture. + */ + +/** + * @typedef AcuantGlobals + * + * @prop {AcuantCameraUI} AcuantCameraUI Acuant camera UI API. + */ + +/** + * @typedef {typeof window & AcuantGlobals} AcuantGlobal + */ /** * @typedef AcuantImage @@ -21,22 +37,36 @@ import PropTypes from 'prop-types'; * @see https://github.com/Acuant/JavascriptWebSDKV11/tree/11.3.3/SimpleHTMLApp#acuantcamera */ +/** + * @typedef {(response:AcuantSuccessResponse)=>void} AcuantSuccessCallback + */ + +/** + * @typedef {(error:Error)=>void} AcuantFailureCallback + */ + /** * @typedef AcuantCaptureCanvasProps * - * @prop {(response:AcuantSuccessResponse)=>void} onImageCaptureSuccess Success callback. - * @prop {(error:Error)=>void} onImageCaptureFailure Failure callback. + * @prop {AcuantSuccessCallback} onImageCaptureSuccess Success callback. + * @prop {AcuantFailureCallback} onImageCaptureFailure Failure callback. */ /** * @param {AcuantCaptureCanvasProps} props Component props. */ -function AcuantCaptureCanvas({ onImageCaptureSuccess, onImageCaptureFailure }) { +function AcuantCaptureCanvas({ + onImageCaptureSuccess = () => {}, + onImageCaptureFailure = () => {}, +}) { useEffect(() => { - window.AcuantCameraUI.start(onImageCaptureSuccess, onImageCaptureFailure); + /** @type {AcuantGlobal} */ (window).AcuantCameraUI.start( + onImageCaptureSuccess, + onImageCaptureFailure, + ); return () => { - window.AcuantCameraUI.end(); + /** @type {AcuantGlobal} */ (window).AcuantCameraUI.end(); }; }, []); @@ -54,14 +84,4 @@ function AcuantCaptureCanvas({ onImageCaptureSuccess, onImageCaptureFailure }) { ); } -AcuantCaptureCanvas.propTypes = { - onImageCaptureSuccess: PropTypes.func, - onImageCaptureFailure: PropTypes.func, -}; - -AcuantCaptureCanvas.defaultProps = { - onImageCaptureSuccess: () => {}, - onImageCaptureFailure: () => {}, -}; - export default AcuantCaptureCanvas; diff --git a/app/javascript/app/document-capture/components/button.jsx b/app/javascript/app/document-capture/components/button.jsx index 54e70bfb692..a20da9dda7d 100644 --- a/app/javascript/app/document-capture/components/button.jsx +++ b/app/javascript/app/document-capture/components/button.jsx @@ -1,8 +1,28 @@ import React from 'react'; -import PropTypes from 'prop-types'; +/** @typedef {import('react').MouseEvent} ReactMouseEvent */ +/** @typedef {import('react').ReactNode} ReactNode */ +/** @typedef {"button"|"reset"|"submit"} ButtonType */ + +/** + * @typedef ButtonProps + * + * @prop {ButtonType=} type Button type, defaulting to "button". + * @prop {(ReactMouseEvent)=>void=} onClick Click handler. + * @prop {ReactNode=} children Element children. + * @prop {boolean=} isPrimary Whether button should be styled as primary button. + * @prop {boolean=} isSecondary Whether button should be styled as secondary button. + * @prop {boolean=} isDisabled Whether button is disabled. + * @prop {boolean=} isUnstyled Whether button should be unstyled, visually as a + * link. + * @prop {string=} className Optional additional class names. + */ + +/** + * @param {ButtonProps} props Props object. + */ function Button({ - type, + type = 'button', onClick, children, isPrimary, @@ -30,26 +50,4 @@ function Button({ ); } -Button.propTypes = { - type: PropTypes.string, - onClick: PropTypes.func, - children: PropTypes.node, - isPrimary: PropTypes.bool, - isSecondary: PropTypes.bool, - isDisabled: PropTypes.bool, - isUnstyled: PropTypes.bool, - className: PropTypes.string, -}; - -Button.defaultProps = { - type: 'button', - onClick: undefined, - children: null, - isPrimary: false, - isSecondary: false, - isDisabled: false, - isUnstyled: false, - className: undefined, -}; - export default Button; diff --git a/app/javascript/app/document-capture/components/document-capture.jsx b/app/javascript/app/document-capture/components/document-capture.jsx index e1ae29a185e..e1ac3cd0d0d 100644 --- a/app/javascript/app/document-capture/components/document-capture.jsx +++ b/app/javascript/app/document-capture/components/document-capture.jsx @@ -1,5 +1,4 @@ import React, { useState, useContext } from 'react'; -import PropTypes from 'prop-types'; import FormSteps from './form-steps'; import DocumentsStep, { isValid as isDocumentsStepValid } from './documents-step'; import SelfieStep, { isValid as isSelfieStepValid } from './selfie-step'; @@ -7,7 +6,17 @@ import MobileIntroStep from './mobile-intro-step'; import DeviceContext from '../context/device'; import Submission from './submission'; -function DocumentCapture({ isLivenessEnabled }) { +/** + * @typedef DocumentCaptureProps + * + * @prop {boolean=} isLivenessEnabled Whether liveness capture should be expected from the user. + * Defaults to false. + */ + +/** + * @param {DocumentCaptureProps} props Props object. + */ +function DocumentCapture({ isLivenessEnabled = true }) { const [formValues, setFormValues] = useState(null); const { isMobile } = useContext(DeviceContext); @@ -35,12 +44,4 @@ function DocumentCapture({ isLivenessEnabled }) { ); } -DocumentCapture.propTypes = { - isLivenessEnabled: PropTypes.bool, -}; - -DocumentCapture.defaultProps = { - isLivenessEnabled: true, -}; - export default DocumentCapture; diff --git a/app/javascript/app/document-capture/components/documents-step.jsx b/app/javascript/app/document-capture/components/documents-step.jsx index feaf617c874..399f88dfcbc 100644 --- a/app/javascript/app/document-capture/components/documents-step.jsx +++ b/app/javascript/app/document-capture/components/documents-step.jsx @@ -1,10 +1,24 @@ import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; import AcuantCapture from './acuant-capture'; import PageHeading from './page-heading'; import useI18n from '../hooks/use-i18n'; import DeviceContext from '../context/device'; -import DataURLFile from '../models/data-url-file'; + +/** @typedef {import('../models/data-url-file')} DataURLFile */ + +/** + * @typedef DocumentsStepValue + * + * @prop {DataURLFile=} front_image Front image value. + * @prop {DataURLFile=} back_image Back image value. + */ + +/** + * @typedef DocumentsStepProps + * + * @prop {DocumentsStepValue=} value Current value. + * @prop {(nextValue:Partial)=>void=} onChange Value change handler. + */ /** * Sides of document to present as file input. @@ -13,7 +27,10 @@ import DataURLFile from '../models/data-url-file'; */ const DOCUMENT_SIDES = ['front', 'back']; -function DocumentsStep({ value, onChange }) { +/** + * @param {DocumentsStepProps} props Props object. + */ +function DocumentsStep({ value = {}, onChange = () => {} }) { const { t } = useI18n(); const { isMobile } = useContext(DeviceContext); @@ -51,19 +68,6 @@ function DocumentsStep({ value, onChange }) { ); } -DocumentsStep.propTypes = { - value: PropTypes.shape({ - front_image: PropTypes.instanceOf(DataURLFile), - back_image: PropTypes.instanceOf(DataURLFile), - }), - onChange: PropTypes.func, -}; - -DocumentsStep.defaultProps = { - value: {}, - onChange: () => {}, -}; - /** * Returns true if the step is valid for the given values, or false otherwise. * diff --git a/app/javascript/app/document-capture/components/file-input.jsx b/app/javascript/app/document-capture/components/file-input.jsx index 58a777d8ab8..38f5dae5fbc 100644 --- a/app/javascript/app/document-capture/components/file-input.jsx +++ b/app/javascript/app/document-capture/components/file-input.jsx @@ -1,11 +1,27 @@ import React, { useContext, useState, useMemo, forwardRef } from 'react'; -import PropTypes from 'prop-types'; import DeviceContext from '../context/device'; import useInstanceId from '../hooks/use-instance-id'; import useIfStillMounted from '../hooks/use-if-still-mounted'; import useI18n from '../hooks/use-i18n'; import DataURLFile from '../models/data-url-file'; +/** @typedef {import('react').MouseEvent} ReactMouseEvent */ +/** @typedef {import('react').ChangeEvent} ReactChangeEvent */ +/** @typedef {import('react').RefAttributes} ReactRefAttributes */ + +/** + * @typedef FileInputProps + * + * @prop {string} label Input label. + * @prop {string=} hint Optional hint text. + * @prop {string=} bannerText Optional banner overlay text. + * @prop {string[]=} accept Optional array of file input accept patterns. + * @prop {DataURLFile=} value Current value. + * @prop {string[]=} errors Errors to show. + * @prop {(ReactMouseEvent)=>void=} onClick Input click handler. + * @prop {(ReactChangeEvent)=>void=} onChange Input change handler. + */ + /** * Given a data URL string, returns the MIME type. * @@ -89,8 +105,20 @@ export function toDataURL(file) { }); } +/** + * @type {import('react').ForwardRefExoticComponent} + */ const FileInput = forwardRef((props, ref) => { - const { label, hint, bannerText, accept, value, errors, onClick, onChange } = props; + const { + label, + hint, + bannerText, + accept, + value, + errors = [], + onClick = () => {}, + onChange = () => {}, + } = props; const { t, formatHTML } = useI18n(); const ifStillMounted = useIfStillMounted(); const instanceId = useInstanceId(); @@ -217,25 +245,4 @@ const FileInput = forwardRef((props, ref) => { ); }); -FileInput.propTypes = { - label: PropTypes.string.isRequired, - hint: PropTypes.string, - bannerText: PropTypes.string, - accept: PropTypes.arrayOf(PropTypes.string), - value: PropTypes.instanceOf(DataURLFile), - errors: PropTypes.arrayOf(PropTypes.string), - onClick: PropTypes.func, - onChange: PropTypes.func, -}; - -FileInput.defaultProps = { - hint: null, - bannerText: null, - accept: null, - value: undefined, - errors: [], - onClick: () => {}, - onChange: () => {}, -}; - export default FileInput; diff --git a/app/javascript/app/document-capture/components/form-steps.jsx b/app/javascript/app/document-capture/components/form-steps.jsx index b1600861239..c95c7092d4e 100644 --- a/app/javascript/app/document-capture/components/form-steps.jsx +++ b/app/javascript/app/document-capture/components/form-steps.jsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; import tabbable from 'tabbable'; import Button from './button'; import useI18n from '../hooks/use-i18n'; @@ -8,10 +7,18 @@ import useHistoryParam from '../hooks/use-history-param'; /** * @typedef FormStep * - * @prop {string} name Step name, used in history parameter. - * @prop {import('react').Component} component Step component implementation. - * @prop {(values:object)=>boolean} isValid Step validity function. Given set of form values, - * returns true if values satisfy requirements. + * @prop {string} name Step name, used in history parameter. + * @prop {import('react').FunctionComponent} component Step component implementation. + * @prop {(values:object)=>boolean=} isValid Step validity function. Given set of form + * values, returns true if values satisfy + * requirements. + */ + +/** + * @typedef FormStepsProps + * + * @prop {FormStep[]=} steps Form steps. + * @prop {(values:Record)=>void=} onComplete Form completion callback. */ /** @@ -54,7 +61,10 @@ export function getLastValidStepIndex(steps, values) { return index === -1 ? steps.length - 1 : index - 1; } -function FormSteps({ steps, onComplete }) { +/** + * @param {FormStepsProps} props Props object. + */ +function FormSteps({ steps = [], onComplete = () => {} }) { const [values, setValues] = useState({}); const formRef = useRef(/** @type {?HTMLFormElement} */ (null)); const isProgressingToNextStep = useRef(false); @@ -152,20 +162,4 @@ function FormSteps({ steps, onComplete }) { ); } -FormSteps.propTypes = { - steps: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - component: PropTypes.elementType.isRequired, - isValid: PropTypes.func, - }), - ), - onComplete: PropTypes.func, -}; - -FormSteps.defaultProps = { - steps: [], - onComplete: () => {}, -}; - export default FormSteps; diff --git a/app/javascript/app/document-capture/components/full-screen.jsx b/app/javascript/app/document-capture/components/full-screen.jsx index 99af2c6fea2..33fe82d42cd 100644 --- a/app/javascript/app/document-capture/components/full-screen.jsx +++ b/app/javascript/app/document-capture/components/full-screen.jsx @@ -1,10 +1,21 @@ import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; import createFocusTrap from 'focus-trap'; import Image from './image'; import useI18n from '../hooks/use-i18n'; -function FullScreen({ onRequestClose, children }) { +/** @typedef {import('react').ReactNode} ReactNode */ + +/** + * @typedef FullScreenProps + * + * @prop {()=>void=} onRequestClose Callback invoked when user initiates close intent. + * @prop {ReactNode} children Child elements. + */ + +/** + * @param {FullScreenProps} props Props object. + */ +function FullScreen({ onRequestClose = () => {}, children }) { const { t } = useI18n(); const modalRef = useRef(/** @type {?HTMLDivElement} */ (null)); const trapRef = useRef(/** @type {?import('focus-trap').FocusTrap} */ (null)); @@ -38,13 +49,4 @@ function FullScreen({ onRequestClose, children }) { ); } -FullScreen.propTypes = { - onRequestClose: PropTypes.func, - children: PropTypes.node.isRequired, -}; - -FullScreen.defaultProps = { - onRequestClose: () => {}, -}; - export default FullScreen; diff --git a/app/javascript/app/document-capture/components/image.jsx b/app/javascript/app/document-capture/components/image.jsx index 26a73bdba3c..0dfc364b1bc 100644 --- a/app/javascript/app/document-capture/components/image.jsx +++ b/app/javascript/app/document-capture/components/image.jsx @@ -1,7 +1,16 @@ import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; import AssetContext from '../context/asset'; +/** + * @typedef ImageProps + * + * @prop {string} assetPath Asset path to resolve. + * @prop {string} alt Image alt attribute. + */ + +/** + * @param {ImageProps & Record} props Props object. + */ function Image({ assetPath, alt, ...imgProps }) { const assets = useContext(AssetContext); @@ -19,9 +28,4 @@ function Image({ assetPath, alt, ...imgProps }) { return {alt}; } -Image.propTypes = { - assetPath: PropTypes.string.isRequired, - alt: PropTypes.string.isRequired, -}; - export default Image; diff --git a/app/javascript/app/document-capture/components/page-heading.jsx b/app/javascript/app/document-capture/components/page-heading.jsx index 03cc776c73d..012c08ad661 100644 --- a/app/javascript/app/document-capture/components/page-heading.jsx +++ b/app/javascript/app/document-capture/components/page-heading.jsx @@ -1,12 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; +/** + * @typedef PageHeadingProps + * + * @prop {import('react').ReactNode} children Child elements. + */ + +/** + * @param {PageHeadingProps} props Props object. + */ function PageHeading({ children }) { return

{children}

; } -PageHeading.propTypes = { - children: PropTypes.node.isRequired, -}; - export default PageHeading; diff --git a/app/javascript/app/document-capture/components/selfie-step.jsx b/app/javascript/app/document-capture/components/selfie-step.jsx index 1fe9709a958..32c93b85428 100644 --- a/app/javascript/app/document-capture/components/selfie-step.jsx +++ b/app/javascript/app/document-capture/components/selfie-step.jsx @@ -1,11 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; import PageHeading from './page-heading'; import useI18n from '../hooks/use-i18n'; import AcuantCapture from './acuant-capture'; -import DataURLFile from '../models/data-url-file'; -function SelfieStep({ value, onChange }) { +/** @typedef {import('../models/data-url-file').default} DataURLFile */ + +/** + * @typedef SelfieStepValue + * + * @prop {DataURLFile=} selfie Selfie value. + */ + +/** + * @typedef SelfieStepProps + * + * @prop {SelfieStepValue=} value Current value. + * @prop {(nextValue:Partial)=>void=} onChange Change handler. + */ + +/** + * @param {SelfieStepProps} props Props object. + */ +function SelfieStep({ value = {}, onChange = () => {} }) { const { t } = useI18n(); return ( @@ -30,18 +46,6 @@ function SelfieStep({ value, onChange }) { ); } -SelfieStep.propTypes = { - value: PropTypes.shape({ - selfie: PropTypes.instanceOf(DataURLFile), - }), - onChange: PropTypes.func, -}; - -SelfieStep.defaultProps = { - value: {}, - onChange: () => {}, -}; - /** * Returns true if the step is valid for the given values, or false otherwise. * diff --git a/app/javascript/app/document-capture/components/submission-complete.jsx b/app/javascript/app/document-capture/components/submission-complete.jsx index 4e045bfa021..a4c8a936800 100644 --- a/app/javascript/app/document-capture/components/submission-complete.jsx +++ b/app/javascript/app/document-capture/components/submission-complete.jsx @@ -1,13 +1,26 @@ -import PropTypes from 'prop-types'; +import React from 'react'; +/** + * @typedef Resource + * + * @prop {()=>T} read Resource reader. + * + * @template T + */ + +/** + * @typedef SubmissionCompleteProps + * + * @prop {Resource} resource Resource object. + */ + +/** + * @param {SubmissionCompleteProps} props Props object. + */ function SubmissionComplete({ resource }) { const response = resource.read(); - return `Finished sending: ${JSON.stringify(response)}`; + return <>Finished sending: {JSON.stringify(response)}; } -SubmissionComplete.propTypes = { - resource: PropTypes.shape({ read: PropTypes.func }), -}; - export default SubmissionComplete; diff --git a/app/javascript/app/document-capture/components/submission.jsx b/app/javascript/app/document-capture/components/submission.jsx index c9940ab1550..4539391bc48 100644 --- a/app/javascript/app/document-capture/components/submission.jsx +++ b/app/javascript/app/document-capture/components/submission.jsx @@ -1,11 +1,19 @@ import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; import useAsync from '../hooks/use-async'; import UploadContext from '../context/upload'; import SuspenseErrorBoundary from './suspense-error-boundary'; import SubmissionComplete from './submission-complete'; import SubmissionPending from './submission-pending'; +/** + * @typedef SubmissionProps + * + * @prop {Record} payload Payload object. + */ + +/** + * @param {SubmissionProps} props Props object. + */ function Submission({ payload }) { const upload = useContext(UploadContext); const resource = useAsync(upload, payload); @@ -17,16 +25,4 @@ function Submission({ payload }) { ); } -Submission.propTypes = { - // Disable reason: While normally its advisable for a components prop shape to - // be well-defined, in this case we expect to be able to send arbitrary data - // to an endpoint. - // eslint-disable-next-line react/forbid-prop-types - payload: PropTypes.any, -}; - -Submission.defaultProps = { - payload: undefined, -}; - export default Submission; diff --git a/app/javascript/app/document-capture/components/suspense-error-boundary.jsx b/app/javascript/app/document-capture/components/suspense-error-boundary.jsx index 7a41888a630..a06b703a85e 100644 --- a/app/javascript/app/document-capture/components/suspense-error-boundary.jsx +++ b/app/javascript/app/document-capture/components/suspense-error-boundary.jsx @@ -1,6 +1,18 @@ import React, { Component, Suspense } from 'react'; -import PropTypes from 'prop-types'; +/** @typedef {import('react').ReactNode} ReactNode */ + +/** + * @typedef SuspenseErrorBoundaryProps + * + * @prop {ReactNode} fallback Fallback to show while suspense pending. + * @prop {ReactNode} errorFallback Fallback to show if suspense resolves as error. + * @prop {ReactNode} children Suspense child. + */ + +/** + * @extends {Component} + */ class SuspenseErrorBoundary extends Component { constructor(props) { super(props); @@ -22,10 +34,4 @@ class SuspenseErrorBoundary extends Component { } } -SuspenseErrorBoundary.propTypes = { - fallback: PropTypes.node.isRequired, - errorFallback: PropTypes.node.isRequired, - children: PropTypes.node.isRequired, -}; - export default SuspenseErrorBoundary; diff --git a/app/javascript/app/document-capture/context/acuant.jsx b/app/javascript/app/document-capture/context/acuant.jsx index 7fd02041a5b..75fecc70e59 100644 --- a/app/javascript/app/document-capture/context/acuant.jsx +++ b/app/javascript/app/document-capture/context/acuant.jsx @@ -1,5 +1,50 @@ import React, { createContext, useMemo, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; + +/** @typedef {import('react').ReactNode} ReactNode */ + +/** + * @typedef AcuantCamera + * + * @prop {boolean} isCameraSupported Whether camera is supported. + */ + +/** + * @typedef AcuantCallbackOptions + * + * @prop {()=>void} onSuccess Success callback. + * @prop {()=>void} onFail Failure callback. + */ + +/** + * @typedef {(credentials:string,endpoint:string,AcuantCallbackOptions)=>void} AcuantInitialize + */ + +/** + * @typedef AcuantJavaScriptWebSDK + * + * @prop {AcuantInitialize} initialize Acuant SDK initializer. + */ + +/** + * @typedef AcuantGlobals + * + * @prop {()=>void} onAcuantSdkLoaded Acuant initialization callback. + * @prop {AcuantCamera} AcuantCamera Acuant camera API. + * @prop {AcuantJavaScriptWebSDK} AcuantJavascriptWebSdk Acuant web SDK. + */ + +/** + * @typedef {typeof window & AcuantGlobals} AcuantGlobal + */ + +/** + * @typedef AcuantContextProviderProps + * + * @prop {string=} sdkSrc SDK source URL. + * @prop {string=} credentials SDK credentials. + * @prop {string=} endpoint Endpoint to submit payload. + * @prop {ReactNode} children Child element. + */ const AcuantContext = createContext({ isReady: false, @@ -9,7 +54,15 @@ const AcuantContext = createContext({ endpoint: null, }); -function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { +/** + * @param {AcuantContextProviderProps} props Props object. + */ +function AcuantContextProvider({ + sdkSrc = '/AcuantJavascriptWebSdk.min.js', + credentials = null, + endpoint = null, + children, +}) { const [isReady, setIsReady] = useState(false); const [isError, setIsError] = useState(false); const [isCameraSupported, setIsCameraSupported] = useState(/** @type {?boolean} */ (null)); @@ -24,15 +77,21 @@ function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { useEffect(() => { // Acuant SDK expects this global to be assigned at the time the script is // loaded, which is why the script element is manually appended to the DOM. - const originalOnAcuantSdkLoaded = window.onAcuantSdkLoaded; - window.onAcuantSdkLoaded = () => { - window.AcuantJavascriptWebSdk.initialize(credentials, endpoint, { - onSuccess: () => { - setIsReady(true); - setIsCameraSupported(window.AcuantCamera.isCameraSupported); + const originalOnAcuantSdkLoaded = /** @type {AcuantGlobal} */ (window).onAcuantSdkLoaded; + /** @type {AcuantGlobal} */ (window).onAcuantSdkLoaded = () => { + /** @type {AcuantGlobal} */ (window).AcuantJavascriptWebSdk.initialize( + credentials, + endpoint, + { + onSuccess: () => { + setIsReady(true); + setIsCameraSupported( + /** @type {AcuantGlobal} */ (window).AcuantCamera.isCameraSupported, + ); + }, + onFail: () => setIsError(true), }, - onFail: () => setIsError(true), - }); + ); }; const script = document.createElement('script'); @@ -42,7 +101,7 @@ function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { document.body.appendChild(script); return () => { - window.onAcuantSdkLoaded = originalOnAcuantSdkLoaded; + /** @type {AcuantGlobal} */ (window).onAcuantSdkLoaded = originalOnAcuantSdkLoaded; document.body.removeChild(script); }; }, []); @@ -50,20 +109,6 @@ function AcuantContextProvider({ sdkSrc, credentials, endpoint, children }) { return {children}; } -AcuantContextProvider.propTypes = { - sdkSrc: PropTypes.string, - credentials: PropTypes.string, - endpoint: PropTypes.string, - children: PropTypes.node, -}; - -AcuantContextProvider.defaultProps = { - sdkSrc: '/AcuantJavascriptWebSdk.min.js', - credentials: null, - endpoint: null, - children: null, -}; - export const Provider = AcuantContextProvider; export default AcuantContext; diff --git a/package.json b/package.json index 8c7d8547989..23e0799ab42 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "jquery": "^3.5.0", "libphonenumber-js": "^1.7.26", "normalize.css": "^4.2.0", - "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", "tabbable": "^4.0.0", From 6406e38e358a534fecab8384d05e7579ad9da907 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 11 Aug 2020 11:41:27 -0400 Subject: [PATCH 2/3] Add TypeScript configuration **Why**: To improve confidence in our component props usage. --- package.json | 3 +++ tsconfig.json | 11 +++++++++++ yarn.lock | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 23e0799ab42..159b98761a2 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "npm": "6.x.x" }, "scripts": { + "typecheck": "tsc", "lint": "eslint app spec --ext .js,.jsx", "test": "mocha 'spec/javascripts/**/**spec.js?(x)'", "build": "true" @@ -37,6 +38,7 @@ "@testing-library/dom": "^7.21.8", "@testing-library/react": "^10.4.8", "@testing-library/user-event": "^12.0.11", + "@types/react": "^16.9.46", "babel-eslint": "^10.1.0", "chai": "^3.5.0", "dirty-chai": "^1.2.2", @@ -52,6 +54,7 @@ "prettier": "^2.0.5", "proxyquire": "^1.8.0", "sinon": "^9.0.2", + "typescript": "^3.9.7", "webpack-dev-server": "^3.11.0" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..d19824e0e58 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "jsx": "react", + "esModuleInterop": true, + "moduleResolution": "node" + }, + "include": ["app/javascript/app/document-capture"] +} diff --git a/yarn.lock b/yarn.lock index 876e6d5ff13..8d2b4d06951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,11 +1239,24 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react@^16.9.46": + version "16.9.46" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e" + integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -3145,6 +3158,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7" + integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -9192,6 +9210,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.9.7: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" From 97d2081997c3ca949cc002b0f7fc45085416ad41 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 11 Aug 2020 11:41:39 -0400 Subject: [PATCH 3/3] Integrate type-checking into CI **Why**: To prevent errors from being inadvertently introduced. --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb6b7e8c82f..255fa3fd8c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,6 +180,7 @@ jobs: name: Run Lints command: | yarn run lint + yarn run typecheck bundle exec slim-lint app/views build-latest-container: working_directory: ~/identity-idp