Advanced Tutorial: KYC Plugin web views
We will go over how to develop home and verification web views for the KYC plugin.
Run the following commands to initialize the plugin web view:
cd web/
npm run add-plugin --plugin=kyc --type=kyc
Run the following commands to build the initial plugin with a default view and install the plugin.
cd ..
npm run build --plugin=kyc
The web view JSON file and bundles are served on the localhost port 8080. In order to allow the kit to access them in plugin development mode, we need to disable the CORS issue on the browser. This can be achieved by using a browser extension.
To start using interactive plugin development mode, we need to run the commands of steps 5 and 6 simultaneously.
Run the following command on the plugin starter kit web directory:
npm start --plugin=kyc
Run the following command on the Hollaex kit web directory:
npm run dev:plugin --plugin=kyc
In order to see the plugin's changes, we need to refresh the page after every change.
Then add the following component to the Form.js file.
Form.js
import React, { useState, useEffect } from 'react';
import { Tabs } from 'antd';
import { withKit } from 'components/KitContext';
import Identity from './components/Identity';
import Documents from './components/Documents';
const { TabPane } = Tabs;
const INITIAL_KYC_TAB_KEY = 'initial_kyc_tab';
const Form = ({ strings: STRINGS, setActivePageContent: setPageContent, handleBack, activeLanguage, user, getCountry, getFormatTimestamp, icons: ICONS }) => {
const kycTabs = [
{
key: 'identity',
title: STRINGS['USER_VERIFICATION.TITLE_IDENTITY'],
},
{
key: 'documents',
title: STRINGS['USER_VERIFICATION.TITLE_ID_DOCUMENTS'],
},
];
const [activeKYCTab, setActiveKYCTab] = useState('identity');
useEffect(() => {
const kycInitialTab = localStorage.getItem(INITIAL_KYC_TAB_KEY);
if (kycInitialTab && kycTabs.map(({key}) => key).includes(kycInitialTab)) {
setActiveKYCTab(kycInitialTab);
}
}, []);
const setActivePageContent = (key) => (page) => {
localStorage.setItem(INITIAL_KYC_TAB_KEY, key);
setPageContent(page)
}
const renderKYCVerificationHomeContent = (key, user, activeLanguage) => {
switch (key) {
case 'identity':
return (
<Identity
activeLanguage={activeLanguage}
user={user}
handleBack={handleBack}
setActivePageContent={setActivePageContent(key)}
getFormatTimestamp={getFormatTimestamp}
getCountry={getCountry}
strings={STRINGS}
/>
);
case 'documents':
return (
<Documents
user={user}
setActivePageContent={setActivePageContent(key)}
icons={ICONS}
strings={STRINGS}
/>
);
default:
return <div>No content</div>;
}
};
return (
<div>
<Tabs activeKey={activeKYCTab} onTabClick={setActiveKYCTab}>
{kycTabs.map(({ key, title }) => (
<TabPane tab={title} key={key}>
{renderKYCVerificationHomeContent(
key,
user,
activeLanguage
)}
</TabPane>
))}
</Tabs>
</div>
)
}
const mapContextToProps = ({ strings, setActivePageContent, handleBack, activeLanguage, user, getCountry, getFormatTimestamp, icons }) => ({
strings,
setActivePageContent,
handleBack,
activeLanguage,
user,
getCountry,
getFormatTimestamp,
icons,
});
export default withKit(mapContextToProps)(Form);
In this component, we are getting the following common props and target-specific props from the kit context using the withKit HOC.
- Common props:
strings
,activeLanguage
,user
,icons
- Target-specific props:
setActivePageContent
,handleBack
,getCountry
,getFormatTimestamp
See the SmartTarget with
REMOTE_COMPONENT__KYC_VERIFICATION_HOME
id on the verification container component for more details about the props.mkdir src/plugins/kyc/views/home/components
touch src/plugins/kyc/views/home/components/Documents.js
touch src/plugins/kyc/views/home/components/Identity.js
Documents.js
import React from 'react';
import moment from 'moment';
import classnames from 'classnames';
import { Button, PanelInformationRow, Editable as EditWrapper, Image } from 'hollaex-web-lib';
const Documents = ({
user,
setActivePageContent,
icons: ICONS,
strings: STRINGS,
}) => {
const { id_data } = user;
let note = '';
if (id_data.status === 1) {
note = STRINGS['USER_VERIFICATION.DOCUMENT_PENDING_NOTE'];
} else if (id_data.status === 2) {
note = id_data.note;
} else {
note = STRINGS['USER_VERIFICATION.DOCUMENT_VERIFIED_NOTE'];
}
return (
<div>
{id_data.status !== 0 && (
<div className="d-flex my-3">
<div
className={classnames('mr-2', 'd-flex', 'justify-content-center', 'align-items-center')}
title={
STRINGS['USER_VERIFICATION.NOTE_FROM_VERIFICATION_DEPARTMENT']
}
>
<Image
icon={ICONS['NOTE_KYC']}
iconId="NOTE_KYC"
stringId="USER_VERIFICATION.NOTE_FROM_VERIFICATION_DEPARTMENT"
wrapperClassName="document-note-icon"
/>
</div>
<PanelInformationRow
stringId="USER_VERIFICATION.CUSTOMER_SUPPORT_MESSAGE,USER_VERIFICATION.DOCUMENT_PENDING_NOTE,USER_VERIFICATION.DOCUMENT_VERIFIED_NOTE"
label={STRINGS['USER_VERIFICATION.CUSTOMER_SUPPORT_MESSAGE']}
information={note}
className="title-font"
disable
/>
</div>
)}
{id_data.status === 1 && (
<div className="my-3">
<PanelInformationRow
stringId="USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_LABEL"
label={
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_LABEL'
]
}
information={id_data.number}
className="title-font"
disable
/>
<div className="d-flex">
<PanelInformationRow
stringId="USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ISSUED_DATE_LABEL"
label={
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ISSUED_DATE_LABEL'
]
}
information={moment(id_data.issued_date).format('DD, MMMM, YYYY')}
className="title-font mr-2"
disable
/>
<PanelInformationRow
stringId="USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.EXPIRATION_DATE_LABEL"
label={
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.EXPIRATION_DATE_LABEL'
]
}
information={moment(id_data.expiration_date).format(
'DD, MMMM, YYYY'
)}
className="title-font"
disable
/>
</div>
</div>
)}
{id_data.status !== 3 && (
<div className="my-2 btn-wrapper">
<div className="holla-verification-button static pt-4">
<EditWrapper stringId="USER_VERIFICATION.START_DOCUMENTATION_SUBMISSION,USER_VERIFICATION.START_DOCUMENTATION_RESUBMISSION" />
<Button
label={
id_data.status === 0
? STRINGS['USER_VERIFICATION.START_DOCUMENTATION_SUBMISSION']
: STRINGS[
'USER_VERIFICATION.START_DOCUMENTATION_RESUBMISSION'
]
}
onClick={() => setActivePageContent('kyc')}
/>
</div>
</div>
)}
</div>
);
};
export default Documents;
Identity.js
import React from 'react';
import { Button, PanelInformationRow, Editable as EditWrapper } from 'hollaex-web-lib';
const formatBirthday = {
en: 'DD, MMMM, YYYY',
fa: 'jDD, jMMMM, jYYYY',
};
const Identity = ({
user,
activeLanguage,
setActivePageContent,
handleBack,
strings: STRINGS,
getCountry,
getFormatTimestamp,
}) => {
const { address, id_data } = user;
if (!address.country) {
return (
<div className="btn-wrapper">
<div className="holla-verification-button static pt-4">
<EditWrapper stringId="USER_VERIFICATION.START_IDENTITY_VERIFICATION" />
<Button
label={STRINGS['USER_VERIFICATION.START_IDENTITY_VERIFICATION']}
onClick={() => setActivePageContent('kyc')}
/>
</div>
</div>
);
} else {
return (
<div>
<div className="font-weight-bold text-lowercase">
{STRINGS.formatString(
STRINGS['USER_VERIFICATION.BANK_VERIFICATION_HELP_TEXT'],
<span
className="verification_link pointer"
onClick={(e) => handleBack('kyc', e)}
>
{STRINGS['USER_VERIFICATION.DOCUMENT_SUBMISSION']}
</span>
)}
<EditWrapper stringId="USER_VERIFICATION.BANK_VERIFICATION_HELP_TEXT,USER_VERIFICATION.DOCUMENT_SUBMISSION" />
</div>
<div className="my-3">
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.FULL_NAME_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.FULL_NAME_LABEL'
]
}
information={user.full_name}
className="title-font"
disable
/>
<div className="d-flex">
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_LABEL,USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_OPTIONS.MAN,USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_OPTIONS.WOMAN"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_LABEL'
]
}
information={
user.gender === false
? STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_OPTIONS.MAN'
]
: STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.GENDER_OPTIONS.WOMAN'
]
}
className="title-font divider"
disable
/>
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.DOB_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.DOB_LABEL'
]
}
information={getFormatTimestamp(
user.dob,
formatBirthday[activeLanguage]
)}
className="title-font"
disable
/>
</div>
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.NATIONALITY_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.NATIONALITY_LABEL'
]
}
information={getCountry(user.nationality).name}
className="title-font"
disable
/>
<div className="d-flex">
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.COUNTRY_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.COUNTRY_LABEL'
]
}
information={getCountry(address.country).name}
className="title-font divider"
disable
/>
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.CITY_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.CITY_LABEL'
]
}
information={address.city}
className="title-font"
disable
/>
</div>
<div className="d-flex">
<div className="w-75">
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.ADDRESS_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.ADDRESS_LABEL'
]
}
information={address.address}
className="title-font divider"
disable
/>
</div>
<PanelInformationRow
stringId="USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.POSTAL_CODE_LABEL"
label={
STRINGS[
'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.POSTAL_CODE_LABEL'
]
}
information={address.postal_code}
className="title-font"
disable
/>
</div>
</div>
{id_data.status === 3 ? null : (
<div className="btn-wrapper">
<div className="holla-verification-button static pt-4">
<EditWrapper stringId="USER_VERIFICATION.REVIEW_IDENTITY_VERIFICATION" />
<Button
label={
STRINGS['USER_VERIFICATION.REVIEW_IDENTITY_VERIFICATION']
}
onClick={() => setActivePageContent('kyc')}
/>
</div>
</div>
)}
</div>
);
}
};
export default Identity;
Form.js
import React, { useState, useEffect } from 'react';
import { Tabs } from 'antd';
import { withKit } from 'components/KitContext';
import Identity from './components/Identity';
import Documents from './components/Documents';
const { TabPane } = Tabs;
const INITIAL_KYC_TAB_KEY = 'initial_kyc_tab';
const Form = ({ handleBack: back, strings: STRINGS, user, constants, identityInitialValues, documentInitialValues }) => {
const kycTabs = [
{
key: 'identity',
title: STRINGS['USER_VERIFICATION.TITLE_IDENTITY'],
},
{
key: 'documents',
title: STRINGS['USER_VERIFICATION.TITLE_ID_DOCUMENTS'],
},
];
const [activeKYCTab, setActiveKYCTab] = useState('identity');
useEffect(() => {
const kycInitialTab = localStorage.getItem(INITIAL_KYC_TAB_KEY);
if (kycInitialTab && kycTabs.map(({key}) => key).includes(kycInitialTab)) {
setActiveKYCTab(kycInitialTab);
}
}, []);
const handleBack = (key) => (params) => {
localStorage.setItem(INITIAL_KYC_TAB_KEY, key);
back(params)
}
const renderKYCVerificationContent = (key) => {
switch (key) {
case 'identity':
return (
<Identity
fullName={user.full_name}
initialValues={identityInitialValues(user, constants)}
handleBack={handleBack(key)}
/>
);
case 'documents':
return (
<Documents
idData={user.id_data}
initialValues={documentInitialValues(user)}
handleBack={handleBack(key)}
/>
);
default:
return <div>No content</div>;
}
};
return (
<div className="presentation_container apply_rtl verification_container">
<Tabs activeKey={activeKYCTab} onTabClick={setActiveKYCTab}>
{kycTabs.map(({ key, title }) => (
<TabPane tab={title} key={key}>
{renderKYCVerificationContent(key)}
</TabPane>
))}
</Tabs>
</div>
)
}
const mapContextToProps = ({ handleBack, strings, user, constants, identityInitialValues, documentInitialValues }) => ({
handleBack,
strings,
user,
constants,
identityInitialValues,
documentInitialValues
});
export default withKit(mapContextToProps)(Form);
In this component, we are getting the following common and target-specific props from the kit context using the withKit HOC.
- Common props:
strings
,user
,constants
- Target-specific props:
handleBack
,identityInitialValues
,documentInitialValues
See the SmartTarget with
REMOTE_COMPONENT__KYC_VERIFICATION
id on the verification container component for more details about the props.mkdir src/plugins/kyc/views/verification/components
touch src/plugins/kyc/views/verification/components/Documents.js
touch src/plugins/kyc/views/verification/components/Identity.js
touch src/plugins/kyc/views/verification/components/HeaderSection.js
Documents.js
import React, { Component } from 'react';
import { reduxForm, SubmissionError, formValueSelector } from 'redux-form';
import { connect } from 'react-redux';
import moment from 'moment';
import {
isBefore,
requiredWithCustomMessage,
} from 'utils';
import { Button, IconTitle, Image, Editable } from 'hollaex-web-lib';
import { HeaderSection } from 'components/HeaderSection';
import { withKit } from 'components/KitContext';
import {
IdentificationFormSection,
PORSection,
SelfieWithPhotoId,
} from './HeaderSection';
import { isMobile } from 'react-device-detect';
const FORM_NAME = 'DocumentsVerification';
const selector = formValueSelector(FORM_NAME);
export const sizeLimitInMB = 6;
const sizeLimit = sizeLimitInMB * 1024 * 1024;
class Documents extends Component {
state = {
formFields: {},
accSize: 0,
};
componentDidMount() {
this.generateFormFields(this.props.activeLanguage);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.activeLanguage !== this.props.activeLanguage) {
this.generateFormFields(nextProps.activeLanguage);
}
if (JSON.stringify(nextProps.files) !== JSON.stringify(this.props.files)) {
this.checkTotalFilesSize(nextProps.files)
}
}
getFileSize = (file) => {
if (file && file.size) {
return file.size;
} else {
return 0;
}
}
checkTotalFilesSize = (files = {}) => {
let accSize = 0
Object.entries(files).forEach(([_, file]) => {
accSize += this.getFileSize(file);
})
this.setState({ accSize });
}
generateFormFields = (language) => {
const { strings: STRINGS } = this.props;
const formFields = {
idDocument: {
number: {
type: 'text',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_PLACEHOLDER,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.ID_NUMBER',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_LABEL'
],
placeholder:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ID_NUMBER_PLACEHOLDER'
],
validate: [
requiredWithCustomMessage(
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.ID_NUMBER'
]
),
],
fullWidth: isMobile,
ishorizontalfield: true,
},
issued_date: {
type: 'date-dropdown',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ISSUED_DATE_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.ISSUED_DATE',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.ISSUED_DATE_LABEL'
],
validate: [
requiredWithCustomMessage(
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.ISSUED_DATE'
]
),
isBefore('', STRINGS['VALIDATIONS.INVALID_DATE']),
],
endDate: moment().add(1, 'days'),
language,
fullWidth: isMobile,
ishorizontalfield: true,
},
expiration_date: {
type: 'date-dropdown',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.EXPIRATION_DATE_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.EXPIRATION_DATE',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.EXPIRATION_DATE_LABEL'
],
validate: [
requiredWithCustomMessage(
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.EXPIRATION_DATE'
]
),
isBefore(moment().add(15, 'years'), STRINGS['VALIDATIONS.INVALID_DATE']),
],
endDate: moment().add(15, 'years'),
addYears: 15,
yearsBefore: 5,
language,
fullWidth: isMobile,
ishorizontalfield: true,
},
},
id: {
type: {
type: 'hidden',
},
front: {
type: 'file',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.FRONT_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.FRONT_PLACEHOLDER,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.FRONT',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.FRONT_LABEL'
],
placeholder:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.FRONT_PLACEHOLDER'
],
validate: [
requiredWithCustomMessage(
STRINGS['USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.FRONT']
),
],
fullWidth: isMobile,
ishorizontalfield: true,
},
},
proof_of_residency: {
type: {
type: 'hidden',
},
back: {
type: 'file',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.POR_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.POR_PLACEHOLDER,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.PROOF_OF_RESIDENCY',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.POR_LABEL'
],
placeholder:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.POR_PLACEHOLDER'
],
validate: [
requiredWithCustomMessage(
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.PROOF_OF_RESIDENCY'
]
),
],
fullWidth: isMobile,
ishorizontalfield: true,
},
},
selfieWithNote: {
type: {
type: 'hidden',
},
proof_of_residency: {
type: 'file',
stringId:
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.SELFIE_PHOTO_ID_LABEL,USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.SELFIE_PHOTO_ID_PLACEHOLDER,USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.SELFIE_PHOTO_ID',
label:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.SELFIE_PHOTO_ID_LABEL'
],
placeholder:
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.FORM_FIELDS.SELFIE_PHOTO_ID_PLACEHOLDER'
],
validate: [
requiredWithCustomMessage(
STRINGS[
'USER_VERIFICATION.ID_DOCUMENTS_FORM.VALIDATIONS.SELFIE_PHOTO_ID'
]
),
],
fullWidth: isMobile,
ishorizontalfield: true,
},
},
};
this.setState({ formFields });
};
handleSubmit = (formValues) => {
const { moveToNextStep, setActivePageContent, updateDocuments } = this.props;
return updateDocuments(formValues)
.then(({ data }) => {
const values = {
type: formValues.type,
number: formValues.number,
expiration_date: formValues.expiration_date,
issued_date: formValues.issued_date,
status: 1,
};
moveToNextStep('documents', values);
setActivePageContent('email');
})
.catch((err) => {
const error = { _error: err.message };
if (err.response && err.response.data) {
error._error = err.response.data.message;
}
throw new SubmissionError(error);
});
};