Skip to content

Commit

Permalink
[WALL] aum / WALL-4454 / refactor-poi-poa-flow-controller (deriv-com#…
Browse files Browse the repository at this point in the history
…15773)

* chore: initial commit

* feat: base components + inital setup

* refactor: TaxInformation module
- Moved PersonalDetails to modules as TaxInformation.
- Refactored TaxInformation with Formik and FormField

* refactor: validation logic for tin number

* refactor: PoaScreen to Poa module
- renamed PoaScreen to Poa module
- refactored AddressSection with Formik and moved it to accounts/modules

* feat: added FormDropdown component

* refactor: DocumentSubmission component for Poa
- Moved DocumentSubmission from account/screens to accounts/modules/Poa/components
- Refactored it to make use of Formik
- Completed Poa flow with API calls for updating address details and uploading poa document

* chore: deleted old components from screens

* refactor: converted IDVDocumentUpload to IDVService
- Moved IDVDocumentUpload from account/screens to IDVService in account/modules
- Refactored it by moving all the logic to useIDVService hook and make use of FormField
- Removed IDVDocumentUpload from account/screens

* refactor: converted VerifyDocumentDetails to VerifyPersonalDetails
- Moved VerifyDocumentDetails as VerifyPersonalDetails
- Refactored it by moving all logic to useVerifyPersonalDetails hook and using FormField

* feat: Added error handling for VerifyPersonalDetails through useIDVService hook

* feat: VerifyPersonalDetails - added error handling and ErrorMessage component

* refactor: useVerifyPersonalDetails and useIDVService
- Done to separate responsibilities for IDVService and VerifyPersonalDetails
- Moved hooks, utils, types to their respective folders for both

* refactor: Moved Onfido from cfd to accounts
- Moved Onfido from features/cfd/screens to features/accounts/modules/DocumentService/components
- Refactored it to use FormField
- Moved all the UI logic to useOnfidoService hook

* feat: added logic for ClientVerification

* refactor: moved UI and API logic for TaxInformation to useTaxInformation hook

* refactor: converted PoiPoaDocsSubmitted to ResubmissionSuccessMessage

* refactor: converted PoiUploadError to UploadErrorMessage

* refactor: ManualService setup + PassportUpload setup

* refactor: SelfieUpload setup + usePassportUpload hook

* refactor: replace Loader with deriv-com/ui

* refactor: setup DrivingLicenseUpload

* refactor: more setup for all Manual flows

* refactor: setup for DocumentService and removed onfido component hook

* feat: added disableAnimation prop form ModalStepWrapper

* fix: bad import failing build

* refactor: fix DatePicker and replace button with deriv-com-ui in Dropzone

* fix: styles for PassportUpload and DocumentRules mapper

* refactor: connect PassportUpload and SelfieUpload + styling

* feat: added documentIssuingCountry prop from Manual POI components

* refactor: added validation schemas for manual forms

* fix: file upload for passport and selfie

* refactor: move Poa logic to usePoa hook

* fix: conflicts with master for WalletsDropdown and Loader

* refactor: applied same method as passport upload to all the other manual upload forms

* fix: desktop styles for manual components

* fix: show manual document selection on retry after failed upload in manual flow

* fix: added documentNumber param to selfie upload
- added documentNumber param to be uploaded with selfie
- helps identifying the selfie for a particular document

* feat: added error screen for duplicate POA document upload

* feat: added onCompletion for ManualService and handle resubmission logic in ClientVerification

* refactor: moved validator to separate file and implemented form Footer

* fix: isUploading logic for manual upload hooks

* fix: behavior and styling issues of base components
- set isInvalid when error for DatePicker
- fix height for error message in Dropzone
- set hasTouched onBlur for FormField to show error after touched

* style: fixed height for input-group and fixed width for divider in manual components

* refactor: expiryDateValidator and remove validations file

* fix: return null after invoking onCompletion for Manual flows
- this also fixed the warning react state update after unmount

* fix: VerifyPersonalDetails checkbox not triggering submission

* feat: added controller logic for DocumentService
- also completed IDVService submission

* chore: bump deriv-com/ui version from 1.29.3 to 1.29.9

* fix: errorMessage shown on initial focus

* refactor: use Dropdown from ui library for FormDropdown
- Also removed WalletDropdown

* refactor: use FormDropdown in IDVService and added case for clients without docs

* refactor: replaced Verification.tsx with ClientVerification.tsx

* chore: remove accounts/screens folder

* fix: minor fixes

* fix: document resubmission

* chore: remove unused unit tests

* chore: reset unwanted files with master

* fix: FormDropdown implementation

* fix: autocomplete for FormField and DatePicker

* chore: remove empty files

* chore: clean up in IDVService

* chore: added documentation for usePoa hook

* chore: update versions for deriv-com/ui and deriv-com/translations

* fix: FormField crashing upon field value length check and PoaUploadErrorMessage icon dims

* fix: NIMC not showing due to invalid config and style fixes

* chore: some cleanup

* fix: IDVService crash on example selection

* refactor: improve errorMessage obj for IDV submission errors

* fix: IDV document type selection dropdown and example for document number field

* feat: changed minimum issuing period message for POA of MF clients

* chore: apply comments

* fix: FormDropdown issue for TaxResidence

* fix: Dropzone styling for Poa

* feat: draft 1 useDocumentUpload

* refactor: replace useDocumentUpload with useDocumentUploadv2

* fix: useOnfido unit test

* chore: applied comments

* fix: sonar issue

* fix: types for useDocumentUpload

* refactor: new impl of useDocumentUpload in PassportUpload and SelfieUpload

* fix: DuplicateDocument error

* refactor: impl new useDocumentUpload in IdentityUpload

* refactor: impl of useDocumentUpload for DrivingLicenseUpload

* refactor: impl new useDocumentUpload for NIMCSlipUpload

* fix: search for FormDropdown

* refactor: impl new useDocumentUpload for Poa

* fix: ci checks

* fix: sonarcloud issues

* fix: more sonarcloud issues

* chore: applied suggestions

* refactor: replace type TDocumentUploadStatus with enum DocumentUploadStatus

* feat: make changes for tablet view

* feat: added localization Footer and Manual flows

* feat: added localization for VerifyPersonalDetails

* feat: added localization for IDV

* feat: added localization for Onfido

* feat: added localization for POA

* feat: added localization for TaxInformation

* fix: type error for availableDocumentOptions useIDVService

* feat: added localization for ClientVerification

* feat: convert maps to getters for localization

* fix: revert suggestion to return values instead of promises in useDocumentUpload

* refactor: convert validation schemas to getters for localization

* chore: comment change

* fix: put onCompletion inside useEffect to trigger only on success

* refactor: remove file deletion for useDocumentUpload

* chore: add translations to ResubmissionSuccessMessage component

* refactor: move SelectedDocument component out of ManualService

* fix: replace localization of template literals with template objects in localize function

* fix: localize function with template literals

* fix: color and pointer for disabled dates in DatePicker

* fix: validation for manual document number field

* feat: custom error message for exceeding file size in dropzone

* fix: client unable to create 2nd MT5 account after POI-POA

* fix: dropzone test case

* fix: added check for previous poi submission to stop showing onfido after idv success
  • Loading branch information
aum-deriv authored Aug 14, 2024
1 parent d6da450 commit 5096e4a
Show file tree
Hide file tree
Showing 221 changed files with 4,479 additions and 2,921 deletions.
2 changes: 1 addition & 1 deletion packages/api-v2/src/hooks/__tests__/useOnfido.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('useOnfido', () => {

// Assert that the necessary data is returned
expect(result.current.isOnfidoInitialized).toBe(true);
expect(result.current.isServiceTokenLoading).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.serviceTokenError).toBeNull();
expect(result.current.onfidoInitializationError).toBeNull();
expect(result.current.data.onfidoRef.current).not.toBeNull();
Expand Down
1 change: 1 addition & 0 deletions packages/api-v2/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ export { default as useExchangeRates } from './useExchangeRates';
export { default as useIsDIELEnabled } from './useIsDIELEnabled';
export { default as useKycAuthStatus } from './useKycAuthStatus';
export { default as useClientCountry } from './useClientCountry';
export { DocumentUploadStatus } from './useDocumentUpload';
209 changes: 138 additions & 71 deletions packages/api-v2/src/hooks/useDocumentUpload.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,151 @@
import { useCallback, useMemo, useState } from 'react';
import useMutation from '../useMutation';
import { compressImageFile, generateChunks, numToUint8Array, readFile } from '../utils';
import { useState } from 'react';
import md5 from 'md5';
import useAPI from '../useAPI';
import { TSocketError, TSocketRequestPayload, TSocketResponse } from '../../types';
import { useAPIContext } from '../APIProvider';
import { compressImageFile, generateChunks, numToUint8Array, readFile } from '../utils';

type TDocumentUploadPayload = Parameters<ReturnType<typeof useMutation<'document_upload'>>['mutate']>[0]['payload'];
type TUploadPayload = Omit<TDocumentUploadPayload, 'document_format' | 'expected_checksum' | 'file_size'> & {
file?: File;
type TDocumentUploadRequest = TSocketRequestPayload<'document_upload'>;
type TDocumentUploadRequestPayload = Partial<TDocumentUploadRequest['payload']> & { file?: File };
type TDocumentUploadResponse = TSocketResponse<'document_upload'> & TSocketError<'document_upload'>;

type TFileInfo = {
fileBuffer: Uint8Array;
fileType: File['type'];
};

/** A custom hook to handle document file uploads to our backend. */
export enum DocumentUploadStatus {
LOADING = 'loading',
IDLE = 'idle',
ERROR = 'error',
SUCCESS = 'success',
}

const REQ_TIMEOUT = 20000;

const useDocumentUpload = () => {
const {
data,
isLoading: _isLoading,
isSuccess: _isSuccess,
mutateAsync,
status,
...rest
} = useMutation('document_upload');
const [isDocumentUploaded, setIsDocumentUploaded] = useState(false);

const { connection } = useAPI();

const isLoading = _isLoading || (!isDocumentUploaded && status === 'success');
const isSuccess = _isSuccess && isDocumentUploaded;

const upload = useCallback(
async (payload: TUploadPayload) => {
if (!payload?.file) return Promise.reject(new Error('No file selected'));
const file = payload.file;
delete payload.file;
const fileBlob = await compressImageFile(file);
const modifiedFile = await readFile(fileBlob);
// @ts-expect-error type mismatch
const fileBuffer = new Uint8Array(modifiedFile.buffer);
const checksum = md5(Array.from(fileBuffer));

const updatedPayload = {
...payload,
document_format: file.type
.split('/')[1]
.toLocaleUpperCase() as TDocumentUploadPayload['document_format'],
expected_checksum: checksum,
file_size: fileBuffer.length,
passthrough: {
document_upload: true,
},
const { wsClient, connection } = useAPIContext();
const [status, setStatus] = useState<DocumentUploadStatus>(DocumentUploadStatus.IDLE);

const getFileInfo = async (file: TDocumentUploadRequestPayload['file']): Promise<TFileInfo> => {
if (!file) return Promise.reject(new Error('No file selected'));

const fileType = file.type;
const fileBlob = await compressImageFile(file);
const modifiedFile = await readFile(fileBlob);
// @ts-expect-error type mismatch
const fileBuffer = new Uint8Array(modifiedFile.buffer);
return { fileBuffer, fileType };
};

/** Perform the initial handshake to get the upload_id from BE */
const handshake = async ({ fileType, fileBuffer }: TFileInfo, payload: TDocumentUploadRequestPayload) => {
const checksum = md5(Array.from(fileBuffer));

const updatedPayload = {
...payload,
document_format: fileType
.split('/')[1]
.toLocaleUpperCase() as TDocumentUploadRequestPayload['document_format'],
expected_checksum: checksum,
file_size: fileBuffer.length,
passthrough: {
document_upload: true,
},
};

try {
const response = (await wsClient.request(
'document_upload',
updatedPayload
)) as Promise<TDocumentUploadResponse>;
return response;
} catch (error) {
return error as TDocumentUploadResponse;
}
};

/** asynchronously sends file data over WS */
const sendFile = (fileBuffer: TFileInfo['fileBuffer'], response: TDocumentUploadResponse) => {
const chunks = generateChunks(fileBuffer, {});
const id = numToUint8Array(response?.document_upload?.upload_id || 0);
const type = numToUint8Array(response?.document_upload?.call_type || 0);

chunks.forEach(chunk => {
const size = numToUint8Array(chunk.length);
const payload = new Uint8Array([...type, ...id, ...size, ...chunk]);
connection?.send(payload);
});
};

/** Initiates file upload and handles the 2nd response received */
const fileUploader = async (fileBuffer: TFileInfo['fileBuffer'], response: TDocumentUploadResponse) => {
/** Request id of the initial document_upload call */
const reqId = response.req_id;
/** Upload id received from BE for the particular file which is appended to every chunk uploaded */
const uploadId = response.document_upload?.upload_id;
/** Timeout reference for removing WS eventListener */
let timeout: NodeJS.Timeout;

return new Promise((resolve, reject) => {
timeout = setTimeout(() => {
wsClient.ws?.removeEventListener('message', handleUploadStatus);
reject(new Error(`Request timeout for document_upload`));
}, REQ_TIMEOUT);

const handleUploadStatus = (messageEvent: MessageEvent) => {
const data = JSON.parse(messageEvent.data) as TDocumentUploadResponse;

if (data.req_id !== reqId && data.document_upload?.upload_id !== uploadId) {
return;
}
if (data.error) {
wsClient.ws?.removeEventListener('message', handleUploadStatus);
clearTimeout(timeout);
setStatus(DocumentUploadStatus.ERROR);
reject(data);
return;
}

if (data.document_upload && data.document_upload?.status === 'failure') {
wsClient.ws?.removeEventListener('message', handleUploadStatus);

clearTimeout(timeout);
setStatus(DocumentUploadStatus.ERROR);
reject(data);
return;
}

if (data.document_upload && data.document_upload?.status === 'success') {
wsClient.ws?.removeEventListener('message', handleUploadStatus);
clearTimeout(timeout);
setStatus(DocumentUploadStatus.SUCCESS);
resolve(data);
}
};
setIsDocumentUploaded(false);
await mutateAsync({ payload: updatedPayload }).then(async res => {
const chunks = generateChunks(fileBuffer, {});
const id = numToUint8Array(res?.document_upload?.upload_id || 0);
const type = numToUint8Array(res?.document_upload?.call_type || 0);

chunks.forEach(chunk => {
const size = numToUint8Array(chunk.length);
const payload = new Uint8Array([...type, ...id, ...size, ...chunk]);
connection?.send(payload);
});
setIsDocumentUploaded(true);
});
},
[connection, mutateAsync]
);

const modified_response = useMemo(() => ({ ...data?.document_upload }), [data?.document_upload]);

wsClient.ws?.addEventListener('message', handleUploadStatus);

sendFile(fileBuffer, response);
}) as Promise<TDocumentUploadResponse>;
};

const upload = async (payload: TDocumentUploadRequestPayload) => {
setStatus(DocumentUploadStatus.LOADING);
const { file, ...rest } = payload;
const fileInfo = await getFileInfo(file);
const handshakeResponse = await handshake(fileInfo, rest);
if (handshakeResponse.error) {
setStatus(DocumentUploadStatus.ERROR);
return Promise.reject(handshakeResponse);
}
const uploadResponse = await fileUploader(fileInfo.fileBuffer, handshakeResponse);
return Promise.resolve(uploadResponse);
};

return {
/** The upload response */
data: modified_response,
/** Function to upload the document */
upload,
/** Mutation status */
status,
/** Whether the mutation is loading */
isLoading,
/** Whether the mutation is successful */
isSuccess,
...rest,
resetStatus: setStatus,
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/api-v2/src/hooks/useOnfido.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const useOnfido = (country?: string, selectedDocument?: string) => {
hasSubmitted,
},
isOnfidoInitialized,
isServiceTokenLoading,
isLoading: isServiceTokenLoading || isOnfidoLoading,
serviceTokenError,
onfidoInitializationError,
};
Expand Down
35 changes: 6 additions & 29 deletions packages/api-v2/src/hooks/usePOI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,13 @@ const usePOI = () => {
const { data: residence_list_data, isSuccess: isResidenceListSuccess } = useResidenceList();
const { data: get_settings_data, isSuccess: isGetSettingsSuccess } = useSettings();

const previous_service = useMemo(() => {
const latest_poi_attempt = authentication_data?.attempts?.latest;
return latest_poi_attempt?.service;
}, [authentication_data?.attempts?.latest]);

/**
* @description Get the previous POI attempts details (if any)
*/
const previous_poi = useMemo(() => {
if (!previous_service) {
return null;
}

const services = authentication_data?.identity?.services;
if (services && services.manual) {
return {
service: previous_service,
status: services.manual.status,
};
}

const current_service = services?.[previous_service as 'idv' | 'onfido'];
return {
service: previous_service,
status: current_service?.status,
reported_properties: current_service?.reported_properties,
last_rejected: current_service?.last_rejected,
submissions_left: current_service?.submissions_left || 0,
};
}, [previous_service, authentication_data?.identity?.services]);
const previous_service = useMemo(() => {
const latest_poi_attempt = authentication_data?.attempts?.latest;
return latest_poi_attempt;
}, [authentication_data?.attempts?.latest]);

/**
* @description Get the current step based on a few checks. Returns configuration for document validation as well.
Expand Down Expand Up @@ -86,15 +63,15 @@ const usePOI = () => {

return {
...authentication_data?.identity,
previous: previous_poi,
previous: previous_service,
current: current_poi,
is_pending: authentication_data?.identity?.status === 'pending',
is_rejected: authentication_data?.identity?.status === 'rejected',
is_expired: authentication_data?.identity?.status === 'expired',
is_suspected: authentication_data?.identity?.status === 'suspected',
is_verified: authentication_data?.identity?.status === 'verified',
};
}, [authentication_data, current_poi, previous_poi]);
}, [authentication_data, current_poi, previous_service]);

return {
data: modified_verification_data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
max-height: calc(var(--wallets-vh, 1vh) * 100 - 10rem);
}

&--disable-animation {
animation: none;

@include mobile {
animation: none;
}
}

&--disable-scroll {
overflow: unset;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WalletText } from '../WalletText';
import './ModalStepWrapper.scss';

type TModalStepWrapperProps = {
disableAnimation?: boolean;
disableScroll?: boolean;
renderFooter?: () => ReactNode;
shouldFixedFooter?: boolean;
Expand All @@ -19,6 +20,7 @@ type TModalStepWrapperProps = {

const ModalStepWrapper: FC<PropsWithChildren<TModalStepWrapperProps>> = ({
children,
disableAnimation = false,
disableScroll = false,
renderFooter,
shouldFixedFooter = true,
Expand Down Expand Up @@ -55,6 +57,7 @@ const ModalStepWrapper: FC<PropsWithChildren<TModalStepWrapperProps>> = ({
return (
<div
className={classNames('wallets-modal-step-wrapper', {
'wallets-modal-step-wrapper--disable-animation': disableAnimation,
'wallets-modal-step-wrapper--disable-scroll': disableScroll,
'wallets-modal-step-wrapper--fixed-footer': fixedFooter && !shouldHideHeader,
'wallets-modal-step-wrapper--hide-deriv-app-header': shouldHideDerivAppHeader,
Expand Down
13 changes: 9 additions & 4 deletions packages/wallets/src/components/DatePicker/DatePicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
button {
border-radius: 0.5rem;
color: var(--text-general, #333333);
cursor: pointer;

&:disabled {
color: #757575;
cursor: not-allowed;
}
}

&__navigation {
Expand All @@ -52,14 +58,13 @@

&__arrow {
font-size: 2.4rem;
transform: translateY(-1.2rem);
justify-content: center;
align-items: center;
text-align: center;
}

&__label {
text-align: center;
font-weight: bold;
transform: translateY(-0.4rem);
font-size: 1.2rem;
}

button {
Expand Down
Loading

0 comments on commit 5096e4a

Please sign in to comment.