├── public ├── manifest.json ├── favicon.ico ├── fonts │ ├── MarkForMC.ttf │ └── MarkForMC-Med.ttf ├── utility.svg ├── external_link.svg ├── mc_symbol.svg ├── index.html ├── list.svg ├── download.svg └── finbank.svg ├── .dockerignore ├── .prettierignore ├── docs ├── lend.png ├── manage.png ├── landing-page.png ├── add-bank-account.png ├── create-customer.png ├── test_case_result.png ├── account-information.png ├── create-customer-auto.png ├── pay_available_balance.png ├── pay_account_owner_details.png └── pay_money_transfer_details.png ├── Dockerfile ├── src ├── components │ ├── Cards │ │ ├── Cards.css │ │ ├── data │ │ │ └── index.ts │ │ └── Cards.tsx │ ├── SandBoxTip │ │ ├── SandBoxTip.css │ │ ├── data │ │ │ └── index.ts │ │ └── SandBoxTip.tsx │ ├── ConnectForm │ │ ├── Forms │ │ │ └── UserNameForm │ │ │ │ ├── UserNameForm.css │ │ │ │ └── UserNameForm.tsx │ │ ├── FormModel │ │ │ ├── connectFormInitialValues.tsx │ │ │ ├── connectFormValidation.tsx │ │ │ └── connectFormModel.tsx │ │ ├── ConnectForm.css │ │ ├── FormFields │ │ │ └── InputField.tsx │ │ ├── data │ │ │ ├── index.ts │ │ │ └── Steps.tsx │ │ └── helper.ts │ ├── Usecases │ │ ├── components │ │ │ ├── index.tsx │ │ │ ├── Manage │ │ │ │ └── Manage.tsx │ │ │ ├── Lend │ │ │ │ ├── Lend.css │ │ │ │ ├── data │ │ │ │ │ └── index.ts │ │ │ │ ├── Lend.tsx │ │ │ │ └── helper.ts │ │ │ └── Pay │ │ │ │ └── Pay.tsx │ │ ├── data │ │ │ └── index.ts │ │ └── Usecases.tsx │ ├── JsonViewer │ │ ├── JsonViewer.css │ │ └── jsonViewer.tsx │ ├── Header │ │ ├── data │ │ │ └── index.ts │ │ ├── Header.css │ │ └── Header.tsx │ ├── AlertBox │ │ ├── data │ │ │ └── index.ts │ │ └── AlertBox.tsx │ ├── Accounts │ │ ├── data │ │ │ └── index.ts │ │ └── Accounts.tsx │ ├── Modal │ │ ├── data │ │ │ └── index.ts │ │ ├── Modal.css │ │ └── Modal.tsx │ ├── ExternalIcon │ │ └── ExternalIcon.tsx │ ├── CurlCommand │ │ ├── CurlCommand.css │ │ ├── helper.ts │ │ └── CurlCommand.tsx │ ├── Product │ │ ├── Product.css │ │ ├── helper.ts │ │ └── Product.tsx │ ├── CircularProgressWithValue │ │ └── CircularProgressWithValue.tsx │ ├── index.tsx │ ├── SnackBarNotification │ │ └── SnackBarNotification.tsx │ ├── ConnectInitiation │ │ └── ConnectInitiation.tsx │ └── DataTable │ │ └── DatatTable.tsx ├── tests │ ├── AlertBox.test.tsx │ ├── Mocks │ │ ├── request-data.ts │ │ └── accounts-data.ts │ ├── ExternalIcon.test.tsx │ ├── Pay.test.tsx │ ├── Manage.test.tsx │ ├── Cards.test.tsx │ ├── Accounts.test.tsx │ ├── CircularProgressWithValue.test.tsx │ ├── jsonViewer.test.tsx │ ├── SandBoxTip.test.tsx │ ├── ConnectInitiation.test.tsx │ ├── CurlCommand.test.tsx │ ├── DataTable.test.tsx │ ├── Usecases.test.tsx │ ├── modal.test.tsx │ ├── Header.test.tsx │ ├── ConnectForm.helper.test.tsx │ ├── utils.helper.test.tsx │ ├── ConnectForm.test.tsx │ ├── SnackBarNotification.test.tsx │ ├── Lend.test.tsx │ ├── Product.test.tsx │ └── product.helper.test.tsx ├── setupTests.ts ├── index.tsx ├── config │ └── index.ts ├── store │ ├── slices │ │ ├── accounts-refreshed.ts │ │ ├── report-progress.ts │ │ └── snackbar.ts │ └── index.ts ├── App.tsx ├── styles │ └── App.css └── utils │ └── helper.ts ├── .prettierrc.json ├── repoTagData.json ├── .env.template ├── compose.yaml ├── tailwind.config.js ├── LICENSE ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── dependabot.yml │ └── main.yml ├── .eslintrc.json ├── sonar-project.properties ├── package.json └── README.md /public/manifest.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Ignore artifacts: 3 | build 4 | coverage -------------------------------------------------------------------------------- /docs/lend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/lend.png -------------------------------------------------------------------------------- /docs/manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/manage.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm i --ignore-scripts 5 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/landing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/landing-page.png -------------------------------------------------------------------------------- /docs/add-bank-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/add-bank-account.png -------------------------------------------------------------------------------- /docs/create-customer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/create-customer.png -------------------------------------------------------------------------------- /docs/test_case_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/test_case_result.png -------------------------------------------------------------------------------- /public/fonts/MarkForMC.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/public/fonts/MarkForMC.ttf -------------------------------------------------------------------------------- /src/components/Cards/Cards.css: -------------------------------------------------------------------------------- 1 | #card-img { 2 | height: 28px; 3 | } 4 | 5 | #card-link { 6 | font-weight: 1000; 7 | } 8 | -------------------------------------------------------------------------------- /docs/account-information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/account-information.png -------------------------------------------------------------------------------- /docs/create-customer-auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/create-customer-auto.png -------------------------------------------------------------------------------- /docs/pay_available_balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/pay_available_balance.png -------------------------------------------------------------------------------- /public/fonts/MarkForMC-Med.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/public/fonts/MarkForMC-Med.ttf -------------------------------------------------------------------------------- /docs/pay_account_owner_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/pay_account_owner_details.png -------------------------------------------------------------------------------- /docs/pay_money_transfer_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/open-banking-reference-application/HEAD/docs/pay_money_transfer_details.png -------------------------------------------------------------------------------- /src/components/SandBoxTip/SandBoxTip.css: -------------------------------------------------------------------------------- 1 | .bg-sandbox { 2 | background: var(--colors-grey-gray-01, #fcfbfa); 3 | border: 1px solid #b1ada6; 4 | border-radius: 8px; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ConnectForm/Forms/UserNameForm/UserNameForm.css: -------------------------------------------------------------------------------- 1 | .info-icon { 2 | color: #585858; 3 | position: relative; 4 | margin-left: 3px; 5 | margin-top: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "jsxSingleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /repoTagData.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentOf": [ 3 | { 4 | "uuid": "b00519d4-437c-3497-a1d3-fd94a84eb684", 5 | "type": "Technical Asset" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Usecases/components/index.tsx: -------------------------------------------------------------------------------- 1 | import Lend from './Lend/Lend'; 2 | import Manage from './Manage/Manage'; 3 | import Pay from './Pay/Pay'; 4 | 5 | export { Lend, Manage, Pay }; 6 | -------------------------------------------------------------------------------- /src/components/JsonViewer/JsonViewer.css: -------------------------------------------------------------------------------- 1 | .json { 2 | margin-top: 1.5rem; 3 | max-height: 25rem; 4 | overflow-y: scroll; 5 | scrollbar-width: none; 6 | scroll-behavior: smooth; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Usecases/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | usecases: ['Lend', 'Manage', 'Pay'], 3 | text: { 4 | waitForReport: 'Please wait while the report is generating.', 5 | }, 6 | }; 7 | 8 | export default data; 9 | -------------------------------------------------------------------------------- /src/components/Header/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | links: { 3 | openbanking: 'https://developer.mastercard.com', 4 | github: 'https://github.com/Mastercard/open-banking-reference-application/', 5 | }, 6 | }; 7 | 8 | export default data; 9 | -------------------------------------------------------------------------------- /src/components/SandBoxTip/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | text: { 3 | tip: 'Please select FinBank Profiles - A and use the following username and password.', 4 | usernameField: 'Banking User ID', 5 | usernameValue: 'test', 6 | }, 7 | }; 8 | 9 | export default data; 10 | -------------------------------------------------------------------------------- /src/components/AlertBox/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | text: { 3 | invalidConfiguration: 'Invalid Configuration', 4 | invalidKeys: 5 | 'Looks like you have configured incorrect Partner ID, Partner Secret, or App key.', 6 | }, 7 | }; 8 | 9 | export default data; 10 | -------------------------------------------------------------------------------- /src/tests/AlertBox.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; // 2 | import AlertBox from '../components/AlertBox/AlertBox'; 3 | describe('Testing AlertBox Component', () => { 4 | test('Should render AlertBox', () => { 5 | render(() as React.ReactElement); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /public/utility.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Your Sandbox API credentials, which can be found in the mastercard developer portal. 2 | # https://developer.mastercard.com/open-banking-us/documentation/quick-start-guide/#1-generate-your-credentials 3 | 4 | REACT_APP_PARTNERID= 5 | REACT_APP_SECRET= 6 | REACT_APP_KEY= 7 | REACT_APP_AUTO_CREATE_CUSTOMER=false -------------------------------------------------------------------------------- /src/components/ConnectForm/FormModel/connectFormInitialValues.tsx: -------------------------------------------------------------------------------- 1 | import { connectFormModel } from './connectFormModel'; 2 | const { 3 | formField: { userName }, 4 | } = connectFormModel; 5 | 6 | const connectFormInitialValues = { 7 | [userName.name]: '', 8 | }; 9 | 10 | export default connectFormInitialValues; 11 | -------------------------------------------------------------------------------- /src/components/Accounts/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | text: { 3 | accountList: 'You have successfully connected', 4 | aboutAccountInfo: 5 | 'using connect. You can now explore our account APIs to fetch such information as account id, name, type, balance, currency etc.', 6 | }, 7 | }; 8 | 9 | export default data; 10 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | open-banking-us: 3 | build: . 4 | environment: 5 | - REACT_APP_PARTNERID=${REACT_APP_PARTNERID} 6 | - REACT_APP_SECRET=${REACT_APP_SECRET} 7 | - REACT_APP_KEY=${REACT_APP_KEY} 8 | - REACT_APP_AUTO_CREATE_CUSTOMER=${REACT_APP_AUTO_CREATE_CUSTOMER} 9 | ports: 10 | - 4000:3000 11 | -------------------------------------------------------------------------------- /src/tests/Mocks/request-data.ts: -------------------------------------------------------------------------------- 1 | import accountData from './accounts-data'; 2 | const requestData: any = { 3 | customerId: '1234567890', 4 | institutionLoginId: '1234567', 5 | token: 'qwertyui', 6 | accountData, 7 | currentAccount: accountData[0], 8 | accountDisplayNames: accountData.map((account: any) => account.name), 9 | }; 10 | 11 | export default requestData; 12 | -------------------------------------------------------------------------------- /public/external_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | #developers-link { 2 | display: flex; 3 | flex-direction: row; 4 | text-decoration: none; 5 | color: white; 6 | } 7 | 8 | #external-link { 9 | padding: 3px; 10 | } 11 | 12 | #github-link { 13 | margin-top: 10px; 14 | } 15 | 16 | #view-github { 17 | text-transform: none; 18 | } 19 | 20 | #view-github:hover { 21 | border: 2px solid #ffffff !important; 22 | } 23 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | process.env.REACT_APP_PARTNERID = '111111'; 7 | process.env.REACT_APP_SECRET = '12345'; 8 | process.env.REACT_APP_KEY = '12345'; 9 | process.env.REACT_APP_AUTO_CREATE_CUSTOMER = 'true'; 10 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Manage/Manage.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@mui/material'; 2 | import Product from '../../../Product/Product'; 3 | 4 | export default function Manage({ requestData }: any) { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store'; 5 | import App from './App'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | /* Configure App key, Partner ID and Partner Secret */ 2 | export const PARTNERID = process.env.REACT_APP_PARTNERID; 3 | export const PARTNERSECRET = process.env.REACT_APP_SECRET; 4 | export const APP_KEY = process.env.REACT_APP_KEY; 5 | export const AUTO_CREATE_CUSTOMER = 6 | process.env.REACT_APP_AUTO_CREATE_CUSTOMER === 'true'; 7 | 8 | /* URL's */ 9 | export const url = { 10 | generateAppToken: '/aggregation/v2/partners/authentication', 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Modal/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | text: { 3 | description: 4 | "Easily connect your customer's financial data to your product.", 5 | more: ' Learn more about the product', 6 | }, 7 | link: { 8 | product: 9 | 'https://developer.mastercard.com/open-banking-us/documentation/', 10 | github: 'https://github.com/Mastercard/open-banking-reference-application', 11 | }, 12 | }; 13 | 14 | export default data; 15 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Lend/Lend.css: -------------------------------------------------------------------------------- 1 | .generate-report__button { 2 | border-radius: 30px !important; 3 | margin-top: -8px !important; 4 | margin-right: 15px !important; 5 | background-color: #2d7763 !important; 6 | padding: 10px 24px !important; 7 | color: #fff !important; 8 | font-size: 14px !important; 9 | line-height: 24px !important; 10 | text-transform: none !important; 11 | font-family: 'MarkForMC-Medium', sans-serif !important; 12 | } 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: { 6 | backgroundImage: { 7 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 8 | 'gradient-conic': 9 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /src/store/slices/accounts-refreshed.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState: any = { refreshed: false }; 4 | const accountsRefreshedSlice = createSlice({ 5 | name: 'accountsRefreshed', 6 | initialState, 7 | reducers: { 8 | refreshed(state: any) { 9 | state.refreshed = true; 10 | }, 11 | }, 12 | }); 13 | 14 | export const accountsRefreshedActions = accountsRefreshedSlice.actions; 15 | export default accountsRefreshedSlice; 16 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import reportProgressSlice from './slices/report-progress'; 3 | import snackBarSlice from './slices/snackbar'; 4 | import accountsRefreshedSlice from './slices/accounts-refreshed'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | reportProgress: reportProgressSlice.reducer, 9 | snackbarState: snackBarSlice.reducer, 10 | accountsRefreshed: accountsRefreshedSlice.reducer, 11 | }, 12 | }); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /src/components/ConnectForm/FormModel/connectFormValidation.tsx: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { connectFormModel } from './connectFormModel'; 3 | 4 | const { 5 | formField: { userName }, 6 | } = connectFormModel; 7 | 8 | const connectFormValidation = [ 9 | yup.object().shape({ 10 | [userName.name]: yup 11 | .string() 12 | .required(`${userName.requiredErrorMessage}`) 13 | .matches(/.\w{5,256}$/, userName.requirementMessage), 14 | }), 15 | ]; 16 | export default connectFormValidation; 17 | -------------------------------------------------------------------------------- /src/components/JsonViewer/jsonViewer.tsx: -------------------------------------------------------------------------------- 1 | import ReactJson from 'react-json-view'; 2 | 3 | import './JsonViewer.css'; 4 | 5 | export default function JsonViewer({ jsonData }: any) { 6 | return ( 7 |
8 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/tests/ExternalIcon.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { ExternalIcon } from '../components'; 3 | 4 | describe('Testing ExternalIcon component', () => { 5 | test('Should render ExternalIcon', () => { 6 | render( 7 | ( 8 | 9 | ) as React.ReactElement 10 | ); 11 | }); 12 | 13 | test('Should render ExternalIcon - 2', () => { 14 | render(() as React.ReactElement); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/ConnectForm/FormModel/connectFormModel.tsx: -------------------------------------------------------------------------------- 1 | export const connectFormModel = { 2 | formId: 'connectForm', 3 | formField: { 4 | userName: { 5 | name: 'userName', 6 | label: 'USERNAME', 7 | requiredErrorMessage: 'username is required', 8 | requirementMessage: 'username does not meet requirements.', 9 | suggestionMessage: 10 | 'minimum 6 characters maximum 255 characters any mix of uppercase, lowercase, numeric, and non-alphabet special characters.', 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Mastercard 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/tests/Pay.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { Pay } from '../components/Usecases/components'; 5 | import store from '../store'; 6 | 7 | import requestData from './Mocks/request-data'; 8 | 9 | describe('Testing manage component', () => { 10 | test('should render manage component', () => { 11 | render( 12 | 13 | 14 | 15 | ); 16 | expect(screen.getByTestId('pay')).toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/ExternalIcon/ExternalIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ExternalIcon({ data = '#8A2D0E', background }: any) { 2 | return ( 3 | 4 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/CurlCommand/CurlCommand.css: -------------------------------------------------------------------------------- 1 | .curl { 2 | background-color: rgb(39, 40, 34); 3 | padding: 10px; 4 | color: #f9f8f5; 5 | font-size: small; 6 | font-weight: normal; 7 | display: block; 8 | overflow-x: auto; 9 | white-space: nowrap; 10 | scrollbar-width: none; 11 | scroll-behavior: smooth; 12 | max-height: 25rem; 13 | overflow-y: scroll; 14 | } 15 | 16 | .curl-value { 17 | color: #fd971f; 18 | } 19 | 20 | .curl-copy { 21 | cursor: pointer; 22 | margin-right: 0 !important; 23 | position: absolute; 24 | right: 50px; 25 | margin-top: 0.5rem; 26 | color: white; 27 | } 28 | -------------------------------------------------------------------------------- /src/tests/Manage.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { Manage } from '../components/Usecases/components'; 5 | import store from '../store'; 6 | 7 | import requestData from './Mocks/request-data'; 8 | 9 | describe('Testing manage component', () => { 10 | test('should render manage component', () => { 11 | render( 12 | 13 | 14 | 15 | ); 16 | expect(screen.getByTestId('manage')).toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | #environment 39 | .env 40 | #vscode 41 | .vscode 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Pay/Pay.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@mui/material'; 2 | 3 | import Product from '../../../Product/Product'; 4 | 5 | export default function Pay({ requestData }: any) { 6 | return ( 7 | 8 | 12 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/store/slices/report-progress.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | const initialState: any = { progress: 0 }; 4 | const reportProgressSlice = createSlice({ 5 | name: 'reportProgress', 6 | initialState, 7 | reducers: { 8 | increaseByvalue(state: any, action: PayloadAction) { 9 | state.progress += action.payload; 10 | }, 11 | absoluteValue(state: any, action: PayloadAction) { 12 | state.progress = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const reportProgressActions = reportProgressSlice.actions; 18 | export default reportProgressSlice; 19 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: maven 9 | directory: '/' 10 | schedule: 11 | interval: daily 12 | time: '04:00' 13 | open-pull-requests-limit: 10 14 | - package-ecosystem: 'github-actions' 15 | directory: '/' 16 | schedule: 17 | interval: 'daily' 18 | -------------------------------------------------------------------------------- /src/tests/Cards.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Cards from '../components/Cards/Cards'; 3 | 4 | describe('Testing Cards Component', () => { 5 | test('Should render API product card', () => { 6 | render(() as React.ReactElement); 7 | expect( 8 | screen.getByText(/api product/i, { exact: false }) 9 | ).toBeInTheDocument(); 10 | }); 11 | 12 | test('Should render quick start guide card', () => { 13 | render(() as React.ReactElement); 14 | expect( 15 | screen.getByText(/quick start guide/i, { exact: false }) 16 | ).toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider, createTheme } from '@mui/material'; 2 | 3 | import { AlertBox, ConnectForm, Header } from './components'; 4 | 5 | import './styles/App.css'; 6 | 7 | export default function App() { 8 | const theme = createTheme({ 9 | typography: { 10 | fontFamily: 'MarkForMC', 11 | }, 12 | }); 13 | 14 | return ( 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module", 15 | "project": ["./tsconfig.json"] 16 | }, 17 | "plugins": ["@typescript-eslint", "react"], 18 | "rules": { 19 | "react/react-in-jsx-scope": "off", 20 | "react/no-unescaped-entities": "off", 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "quotes": [2, "single", { "avoidEscape": true }] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/tests/Accounts.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Accounts from '../components/Accounts/Accounts'; 3 | import requestData from './Mocks/request-data'; 4 | import { Provider } from 'react-redux'; 5 | import store from '../store'; 6 | describe('Testing Accounts Component', () => { 7 | test('Should render Accounts', () => { 8 | render( 9 | ( 10 | 11 | 12 | 13 | ) as React.ReactElement 14 | ); 15 | 16 | const accounts = screen.getByTestId('accounts-test'); 17 | expect(accounts.textContent).toContain('using connect'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/tests/CircularProgressWithValue.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import CircularProgressWithValue from '../components/CircularProgressWithValue/CircularProgressWithValue'; 3 | 4 | describe('Testing CircularProgressWithValue Component', () => { 5 | test('Should render CircularProgressWithValue', () => { 6 | render( 7 | ( 8 | 15 | ) as React.ReactElement 16 | ); 17 | const progressBar = screen.getByTestId('progressBar'); 18 | expect(progressBar).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Cards/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | cardsData: [ 3 | { 4 | logo: '/list.svg', 5 | title: 'API Products', 6 | description: 7 | 'Check out the full product catalog of Mastercard Open Banking Services APIs', 8 | href: 'https://developer.mastercard.com/open-banking-us/documentation/', 9 | anchorTitle: 'Learn more', 10 | }, 11 | { 12 | logo: '/download.svg', 13 | title: 'Quick start guide', 14 | description: 15 | 'Learn about key concepts and find out how to quickly connect to our service', 16 | href: 'https://developer.mastercard.com/open-banking-us/documentation/quick-start-guide/', 17 | anchorTitle: 'Learn more', 18 | }, 19 | ], 20 | }; 21 | 22 | export default data; 23 | -------------------------------------------------------------------------------- /src/components/Product/Product.css: -------------------------------------------------------------------------------- 1 | .arrow { 2 | margin-bottom: 3px; 3 | color: #f37338; 4 | } 5 | 6 | .curl-text { 7 | color: #f37338; 8 | } 9 | 10 | .curl-text-close { 11 | font-weight: normal; 12 | } 13 | 14 | .curl-text-open { 15 | font-weight: bold; 16 | } 17 | 18 | .fetch_button_group { 19 | margin-left: auto !important; 20 | margin-right: 0 !important; 21 | } 22 | 23 | .fetch_button { 24 | border-radius: 5px !important; 25 | background-color: #2d7763 !important; 26 | padding: 10px 24px !important; 27 | color: #fff !important; 28 | font-size: 14px !important; 29 | line-height: 24px !important; 30 | min-width: 100px; 31 | text-transform: none !important; 32 | font-family: 'MarkForMC-Medium', sans-serif !important; 33 | } 34 | 35 | .disable_fetch_button { 36 | opacity: 0.6; 37 | cursor: not-allowed; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/ConnectForm/ConnectForm.css: -------------------------------------------------------------------------------- 1 | #connectForm { 2 | width: inherit; 3 | } 4 | 5 | .step { 6 | word-wrap: break-word; 7 | color: #111; 8 | } 9 | 10 | .connect-form__button { 11 | border-radius: 30px !important; 12 | margin-top: 15px !important; 13 | background-color: #2d7763 !important; 14 | padding: 10px 24px !important; 15 | color: #fff !important; 16 | font-size: 14px !important; 17 | line-height: 24px !important; 18 | text-transform: none !important; 19 | font-family: 'MarkForMC-Medium', sans-serif !important; 20 | } 21 | 22 | .reset-demo__button { 23 | border-radius: 30px !important; 24 | margin-top: 20px !important; 25 | padding: 10px 24px !important; 26 | background-color: #fff !important; 27 | color: #111 !important; 28 | text-transform: none !important; 29 | font-family: 'MarkForMC-Medium', sans-serif !important; 30 | } 31 | -------------------------------------------------------------------------------- /src/tests/jsonViewer.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { JsonViewer } from '../components'; 3 | 4 | describe('Testing Json Viewer Component', () => { 5 | test('Should render API product card', () => { 6 | const jsonData = [ 7 | { 8 | id: 15430035, 9 | realAccountNumberLast4: '1111', 10 | availableBalance: 801.39, 11 | availableBalanceDate: 1722505195, 12 | clearedBalance: 801.39, 13 | clearedBalanceDate: 1722505195, 14 | aggregationStatusCode: 0, 15 | currency: 'AUD', 16 | }, 17 | ]; 18 | render(() as React.ReactElement); 19 | expect( 20 | screen.getByText(/realAccountNumberLast4/i, { exact: false }) 21 | ).toBeInTheDocument(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Mastercard_open-banking-reference-application 2 | sonar.organization=mastercard 3 | sonar.projectName=open-banking-reference-application 4 | sonar.projectVersion=1.0 5 | 6 | # Exclusions parameters 7 | sonar.exclusions=Dockerfile,src/tests/**,public/** 8 | 9 | # Test coverage parameters 10 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info 11 | sonar.coverage.exclusions=src/**/*.ts,src/*.js,src/App.tsx,src/index.tsx,/*.js 12 | 13 | # Duplication exclusion parameters 14 | sonar.cpd.exclusions=src/components/Product/data/index.ts,src/components/Usecases/components/Lend/data/index.ts 15 | 16 | # This is the name and version displayed in the SonarCloud UI. 17 | 18 | 19 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 20 | #sonar.sources=. 21 | 22 | # Encoding of the source code. Default is default system encoding 23 | #sonar.sourceEncoding=UTF-8 24 | -------------------------------------------------------------------------------- /src/components/ConnectForm/FormFields/InputField.tsx: -------------------------------------------------------------------------------- 1 | import { at } from 'lodash'; 2 | import { useField } from 'formik'; 3 | import TextField from '@mui/material/TextField'; 4 | 5 | export default function InputField(props: any) { 6 | const { ...rest } = props; 7 | const [field, meta] = useField(props); 8 | const isError = true; 9 | function _renderHelperText() { 10 | const [touched, error] = at(meta, 'touched', 'error'); 11 | if (touched && error) { 12 | return error; 13 | } 14 | } 15 | 16 | return ( 17 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /public/mc_symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/store/slices/snackbar.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | const initialState: any = { 4 | open: false, 5 | message: 'Something went wrong', 6 | severity: 'error', 7 | timeout: 5000, 8 | }; 9 | const snackBarSlice = createSlice({ 10 | name: 'snackbarState', 11 | initialState, 12 | reducers: { 13 | open(state: any, action: PayloadAction) { 14 | state.open = true; 15 | state.severity = action.payload.severity || state.severity; 16 | state.message = 17 | action.payload.message || 18 | (state.severity === 'error' 19 | ? 'Something went wrong' 20 | : state.message); 21 | state.timeout = action.payload.timeout || state.timeout; 22 | }, 23 | close(state: any) { 24 | state.open = false; 25 | }, 26 | }, 27 | }); 28 | 29 | export const snackbarActions = snackBarSlice.actions; 30 | export default snackBarSlice; 31 | -------------------------------------------------------------------------------- /src/components/Accounts/Accounts.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Divider, Stack, Typography } from '@mui/material'; 2 | 3 | import { Product } from '../../components'; 4 | 5 | import data from './data'; 6 | 7 | export default function Accounts({ requestData }: any) { 8 | return ( 9 | 10 | 11 | 12 | {data.text.accountList + ' '} 13 | 14 | {requestData.accountData.length} accounts 15 | {' '} 16 | {data.text.aboutAccountInfo} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [main, staging, develop, feature/workflow] 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | schedule: 8 | # * is a special character in YAML so you have to quote this string 9 | - cron: '0 12 * * *' 10 | jobs: 11 | sonarcloud: 12 | name: SonarCloud 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 18 | - name: Install dependencies 19 | run: npm install 20 | - name: Test and coverage 21 | run: npm run test:coverage 22 | - name: SonarCloud Scan 23 | uses: SonarSource/sonarcloud-github-action@master 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 26 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | Open Banking Reference App 14 | 15 | 16 | 17 |
18 | 28 | 29 | -------------------------------------------------------------------------------- /src/tests/SandBoxTip.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import SandBoxTip from '../components/SandBoxTip/SandBoxTip'; 3 | 4 | describe('Testing SandboxTip component', () => { 5 | test('Should render SanboxTip component', () => { 6 | render(() as React.ReactElement); 7 | expect( 8 | screen.getByText(/sandbox tip/i, { exact: false }) 9 | ).toBeInTheDocument(); 10 | }); 11 | 12 | test('Sandbox tip should have Institution field', () => { 13 | render(() as React.ReactElement); 14 | expect( 15 | screen.getByText(/institution/i, { exact: false }) 16 | ).toBeInTheDocument(); 17 | }); 18 | 19 | test('Sandbox tip should have email field', () => { 20 | render(() as React.ReactElement); 21 | expect( 22 | screen.getByText(/User ID/i, { exact: false }) 23 | ).toBeInTheDocument(); 24 | }); 25 | 26 | test('Sandbox tip should have password field', () => { 27 | render(() as React.ReactElement); 28 | expect( 29 | screen.getByText(/Banking Password/i, { exact: false }) 30 | ).toBeInTheDocument(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/CircularProgressWithValue/CircularProgressWithValue.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CircularProgress, 3 | CircularProgressProps, 4 | Typography, 5 | Box, 6 | } from '@mui/material'; 7 | 8 | export default function CircularProgressWithValue( 9 | props: CircularProgressProps & { value: number } 10 | ) { 11 | return ( 12 | 13 | 14 | 26 | {`${Math.round(props.value)}%`} 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | import Accounts from './Accounts/Accounts'; 2 | import AlertBox from './AlertBox/AlertBox'; 3 | import Cards from './Cards/Cards'; 4 | import CircularProgressWithValue from './CircularProgressWithValue/CircularProgressWithValue'; 5 | import ConnectForm from './ConnectForm/ConnectForm'; 6 | import ConnectInitiation from './ConnectInitiation/ConnectInitiation'; 7 | import CurlCommand from './CurlCommand/CurlCommand'; 8 | import DataTable from './DataTable/DatatTable'; 9 | import ExternalIcon from './ExternalIcon/ExternalIcon'; 10 | import Header from './Header/Header'; 11 | import JsonViewer from './JsonViewer/jsonViewer'; 12 | import Modal from './Modal/Modal'; 13 | import Product from './Product/Product'; 14 | import SandBoxTip from './SandBoxTip/SandBoxTip'; 15 | import SnackBarNotification from './SnackBarNotification/SnackBarNotification'; 16 | import Usecases from './Usecases/Usecases'; 17 | 18 | export { 19 | Accounts, 20 | AlertBox, 21 | Cards, 22 | CircularProgressWithValue, 23 | ConnectForm, 24 | ConnectInitiation, 25 | CurlCommand, 26 | DataTable, 27 | ExternalIcon, 28 | Header, 29 | JsonViewer, 30 | Modal, 31 | Product, 32 | SandBoxTip, 33 | SnackBarNotification, 34 | Usecases, 35 | }; 36 | -------------------------------------------------------------------------------- /public/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ConnectForm/Forms/UserNameForm/UserNameForm.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Stack, Typography, Tooltip } from '@mui/material'; 2 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 3 | 4 | import InputField from '../../FormFields/InputField'; 5 | 6 | import './UserNameForm.css'; 7 | 8 | export default function UserNameForm(props: any) { 9 | const { 10 | formField: { userName }, 11 | } = props; 12 | return ( 13 | 14 | 15 | 16 | 17 | Username 18 | 19 | 20 | Enter a unique identifier for the customer 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/tests/ConnectInitiation.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import ConnectInitiation from '../components/ConnectInitiation/ConnectInitiation'; 3 | 4 | const userData = { 5 | username: 'testing-customer', 6 | createdDate: Date.now(), 7 | }; 8 | 9 | describe('Testing ConnectInitiation component', () => { 10 | test('Should render ConnectInitiation component', () => { 11 | render(() as React.ReactElement); 12 | expect(screen.getByTestId('customer-name')).toBeInTheDocument(); 13 | expect( 14 | screen.getAllByTestId('customer-created-date')[0] 15 | ).toBeInTheDocument(); 16 | }); 17 | 18 | test('Should have customer name', () => { 19 | render(() as React.ReactElement); 20 | const customerNameElement = screen.getByTestId('customer-name'); 21 | expect(customerNameElement.textContent).toEqual('John Smith'); 22 | }); 23 | 24 | test('Should have customer created date', () => { 25 | render(() as React.ReactElement); 26 | const customerNameElement = screen.getAllByTestId( 27 | 'customer-created-date' 28 | ); 29 | expect(customerNameElement[2].textContent).toEqual( 30 | new Date(userData.createdDate * 1000).toDateString() 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/tests/CurlCommand.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { CurlCommand } from '../components'; 4 | 5 | import data from '../components/Product/data'; 6 | import requestData from './Mocks/request-data'; 7 | const writeText = jest.fn(); 8 | 9 | Object.assign(navigator, { 10 | clipboard: { 11 | writeText, 12 | }, 13 | }); 14 | describe('Testing curl command Component', () => { 15 | test('Should open snack bar when severity error', () => { 16 | render( 17 | ( 18 | 22 | ) as React.ReactElement 23 | ); 24 | expect(screen.getByText(/curl --location/i)).toBeInTheDocument(); 25 | }); 26 | 27 | test('Should copy To Clip Board', async () => { 28 | render( 29 | ( 30 | 35 | ) as React.ReactElement 36 | ); 37 | navigator.clipboard.writeText = jest.fn().mockReturnValueOnce('copied'); 38 | await userEvent.click(screen.getByTestId('copyToClipBoard')); 39 | await screen.findByText('copied!'); 40 | await screen.findByText('copy to clipboard', {}, { timeout: 2000 }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/AlertBox/AlertBox.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | Alert, 8 | } from '@mui/material'; 9 | 10 | import { PARTNERID, PARTNERSECRET, APP_KEY } from '../../config'; 11 | 12 | import data from './data'; 13 | 14 | export default function AlertBox() { 15 | const [openAlert] = useState(!(PARTNERID && PARTNERSECRET && APP_KEY)); 16 | 17 | return ( 18 | 24 | 31 | {data.text.invalidConfiguration} 32 | 33 | 34 | 35 | 42 |
{data.text.invalidKeys}
43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/tests/DataTable.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { DataTable } from '../components'; 3 | 4 | describe('Testing data table Component', () => { 5 | const columns = [ 6 | { 7 | accessorKey: 'id', 8 | header: 'Account Id', 9 | }, 10 | { 11 | accessorKey: 'accountNumber', 12 | header: 'Account Number', 13 | }, 14 | { 15 | accessorKey: 'availableBalance', 16 | header: 'Available Balance', 17 | }, 18 | { 19 | accessorKey: 'currency', 20 | header: 'Currency', 21 | }, 22 | ]; 23 | const data = [ 24 | { 25 | id: '15430035', 26 | accountNumber: '1111', 27 | availableBalance: 801.39, 28 | currency: 'AUD', 29 | }, 30 | { 31 | id: '15430036', 32 | accountNumber: '1112', 33 | availableBalance: 801.39, 34 | currency: 'AUD', 35 | }, 36 | ]; 37 | test('Should create data table', () => { 38 | const product = 'Transactions'; 39 | render( 40 | ( 41 | 42 | ) as React.ReactElement 43 | ); 44 | expect(screen.getByTestId('table')).toBeInTheDocument(); 45 | }); 46 | 47 | test('Should create data table with available balance - live', () => { 48 | const product = 'Available balance - Live'; 49 | render( 50 | ( 51 | 52 | ) as React.ReactElement 53 | ); 54 | expect(screen.getByTestId('table')).toBeInTheDocument(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | #product-img { 2 | margin-top: 1px; 3 | } 4 | 5 | .modal-dialog-parent { 6 | display: flex; 7 | width: 462px; 8 | padding: 40px; 9 | flex-direction: column; 10 | align-items: flex-start; 11 | gap: 80px; 12 | } 13 | 14 | .modal-dialog-content { 15 | display: flex; 16 | justify-content: space-between; 17 | flex-direction: column; 18 | height: 350px; 19 | } 20 | 21 | .ref-text { 22 | font-size: 12px !important; 23 | font-weight: 700 !important; 24 | line-height: 16px !important; 25 | letter-spacing: 1.8px; 26 | text-transform: uppercase; 27 | margin-bottom: 8px !important; 28 | color: var(--text-text-primary, #111); 29 | } 30 | 31 | .open-banking-text { 32 | margin-bottom: 16px !important; 33 | font-size: 24px !important; 34 | font-weight: 400 !important; 35 | line-height: 32px !important; 36 | color: var(--text-text-primary, #111); 37 | } 38 | 39 | .view-on-github__button { 40 | border: 2px solid var(--surfaces-surface-button, #cf4500); 41 | border-radius: 30px !important; 42 | padding: 10px 24px !important; 43 | color: #cf4500 !important; 44 | border-color: #cf4500 !important; 45 | text-transform: none !important; 46 | font-family: 'MarkForMC-Medium', sans-serif !important; 47 | font-size: 16px !important; 48 | line-height: 24px !important; 49 | font-weight: 500 !important; 50 | } 51 | 52 | .demo__button { 53 | border-radius: 30px !important; 54 | background-color: #cf4500 !important; 55 | padding: 10px 24px !important; 56 | color: #ffffff !important; 57 | text-transform: none !important; 58 | font-family: 'MarkForMC-Medium', sans-serif !important; 59 | font-size: 16px !important; 60 | line-height: 24px !important; 61 | font-weight: 500 !important; 62 | } 63 | -------------------------------------------------------------------------------- /src/tests/Usecases.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { Usecases } from '../components'; 5 | import store from '../store'; 6 | 7 | import requestData from './Mocks/request-data'; 8 | import userEvent from '@testing-library/user-event'; 9 | 10 | describe('Testing usecases component', () => { 11 | beforeEach(() => { 12 | jest.resetModules(); // Clears the cache 13 | }); 14 | afterEach(() => { 15 | jest.restoreAllMocks(); // Restores all mocks to their original implementation 16 | jest.resetAllMocks(); 17 | }); 18 | test('Should render usecase component', async () => { 19 | render( 20 | 21 | 22 | 23 | ); 24 | expect(screen.getByTestId('usecases')).toBeInTheDocument(); 25 | 26 | const generate = await screen.findByTestId('generate-report'); 27 | await userEvent.click(generate); 28 | await userEvent.click(await screen.findByText('Manage')); 29 | }); 30 | 31 | test('Should render usecase component and validate handleReportChangeTabs', async () => { 32 | jest.mock('../components/Usecases/components/Lend/helper', () => { 33 | return { 34 | submitReport: jest.fn(() => { 35 | setTimeout(() => 'timeout', 3); 36 | }), 37 | }; 38 | }); 39 | render( 40 | 41 | 42 | 43 | ); 44 | expect(screen.getByTestId('usecases')).toBeInTheDocument(); 45 | 46 | const generate = await screen.findByTestId('generate-report'); 47 | userEvent.click(generate); 48 | await userEvent.click(await screen.findByText('Manage')); 49 | await userEvent.click(await screen.findByText('Pay')); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/CurlCommand/helper.ts: -------------------------------------------------------------------------------- 1 | import { APP_KEY } from '../../config'; 2 | 3 | /** 4 | * Get curl command for given product API 5 | * @param product product details 6 | * @param requestData application parameters 7 | * @param body request body 8 | * @returns curl command parameters 9 | */ 10 | export const getCurlCommandParameters = ( 11 | product: any, 12 | requestData: any, 13 | body: any 14 | ) => { 15 | const todayDate = new Date(); 16 | const endDate = Math.floor(todayDate.getTime() / 1000); 17 | todayDate.setMonth(todayDate.getMonth() - 1); 18 | const startDate = Math.floor(todayDate.getTime() / 1000); 19 | const url = 20 | 'https://api.finicity.com' + 21 | product.api 22 | .replace('', requestData.customerId) 23 | .replace('', requestData.institutionLoginId) 24 | .replace('', requestData.currentAccount.id) 25 | .replace('', startDate) 26 | .replace('', endDate); 27 | 28 | const headers = []; 29 | let printHeaders = ''; 30 | headers.push({ key: 'Content-Type', value: 'application/json' }); 31 | printHeaders += '--header ' + '"Content-Type: application/json"' + ' '; 32 | headers.push({ key: 'Accept', value: 'application/json' }); 33 | printHeaders += '--header ' + '"Accept: application/json"' + ' '; 34 | headers.push({ key: 'Finicity-App-Key', value: APP_KEY }); 35 | printHeaders += '--header ' + `"Finicity-App-Key: ${APP_KEY}"` + ' '; 36 | if (requestData.token) { 37 | headers.push({ key: 'Finicity-App-Token', value: requestData.token }); 38 | printHeaders += 39 | '--header ' + `"Finicity-App-Token: ${requestData.token}"` + ' '; 40 | } 41 | return { 42 | url, 43 | headers, 44 | dataRaw: JSON.parse(body || '{}'), 45 | command: `curl --location --request ${ 46 | body ? 'POST' : 'GET' 47 | } "${url}" ${printHeaders}--data-raw ${JSON.stringify(body)}`, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/ConnectForm/data/index.ts: -------------------------------------------------------------------------------- 1 | const data = { 2 | url: { 3 | generateAppToken: '/aggregation/v2/partners/authentication', 4 | activateCustomer: '/aggregation/v2/customers/testing', 5 | generateConnectUrl: '/connect/v2/generate', 6 | createConsumer: '/decisioning/v1/customers//consumer', 7 | refreshAccounts: 8 | '/aggregation/v1/customers//institutionLogins//accounts', 9 | }, 10 | body: { 11 | createConsumer: JSON.stringify({ 12 | firstName: 'Homer', 13 | lastName: 'Loanseeker', 14 | address: '123 FAKE ST', 15 | city: 'OGDEN', 16 | state: 'UT', 17 | zip: '84401', 18 | phone: '1-800-986-3343', 19 | ssn: '999601111', 20 | birthday: { 21 | year: 1970, 22 | month: 7, 23 | dayOfMonth: 4, 24 | }, 25 | email: 'myname@mycompany.com', 26 | suffix: 'Mr.', 27 | }), 28 | activateCustomer: JSON.stringify({ 29 | firstName: 'John', 30 | lastName: 'Smith', 31 | email: `john_smith_${Date.now()}@domain.com`, 32 | phone: '6786786786', 33 | }), 34 | }, 35 | fixedAccordions: ['panel0', 'panel1'], 36 | accordions: [ 37 | { 38 | id: 'panel0', 39 | title: 'CREATE CUSTOMER', 40 | content: '', 41 | nextId: 'panel1', 42 | }, 43 | { 44 | id: 'panel1', 45 | title: 'CONNECT BANK ACCOUNT', 46 | content: '', 47 | nextId: 'panel2', 48 | }, 49 | { 50 | id: 'panel2', 51 | title: 'ACCOUNT INFORMATION', 52 | content: '', 53 | nextId: 'panel3', 54 | }, 55 | { 56 | id: 'panel3', 57 | title: 'USE CASES', 58 | content: '', 59 | nextId: 'panel4', 60 | }, 61 | ], 62 | }; 63 | 64 | export default data; 65 | -------------------------------------------------------------------------------- /src/styles/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: 'MarkForMC'; 7 | src: url('../../public/fonts/MarkForMC.ttf'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'MarkForMC-Medium'; 14 | src: url('../../public/fonts/MarkForMC-Med.ttf'); 15 | font-weight: bold; 16 | font-style: normal; 17 | } 18 | 19 | :root { 20 | --foreground-rgb: 0, 0, 0; 21 | --background-start-rgb: 214, 219, 220; 22 | --background-end-rgb: 255, 255, 255; 23 | } 24 | 25 | @media (prefers-color-scheme: dark) { 26 | :root { 27 | --foreground-rgb: 255, 255, 255; 28 | --background-start-rgb: 0, 0, 0; 29 | --background-end-rgb: 0, 0, 0; 30 | } 31 | } 32 | 33 | body { 34 | color: rgb(var(--foreground-rgb)); 35 | background: rgb(240, 236, 232); 36 | max-height: fit-content; 37 | font-family: 'MarkForMC', sans-serif; 38 | } 39 | 40 | p { 41 | color: var(--text-text-primary, #111) !important; 42 | } 43 | 44 | iframe { 45 | @apply min-h-[105vh]; 46 | } 47 | 48 | @media (min-width: 641px) and (max-width: 1120px) { 49 | iframe { 50 | @apply min-h-[110vh]; 51 | } 52 | } 53 | 54 | .MuiAccordion-root.custom-accordion::before { 55 | background-color: transparent; 56 | } 57 | 58 | .MuiAccordion-root.custom-accordion { 59 | box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.1); 60 | } 61 | 62 | .MuiInputBase-input.MuiOutlinedInput-input { 63 | width: 288px; 64 | } 65 | 66 | .create-customer-stepper .MuiStepConnector-line { 67 | min-height: 0px !important; 68 | } 69 | 70 | .css-1aquho2-MuiTabs-indicator { 71 | background-color: #2d7763 !important; 72 | } 73 | 74 | .css-1hgnj26-MuiButtonBase-root-MuiTab-root.Mui-selected { 75 | color: #2d7763 !important; 76 | } 77 | 78 | .css-1xqtpac-MuiButtonBase-root-MuiToggleButton-root.Mui-selected { 79 | color: #2d7763 !important; 80 | font-weight: 600; 81 | } 82 | 83 | .css-1hgnj26-MuiButtonBase-root-MuiTab-root { 84 | width: 33%; 85 | } 86 | 87 | .css-rorn0c-MuiTableContainer-root { 88 | margin-top: -2rem; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/SandBoxTip/SandBoxTip.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, Typography } from '@mui/material'; 2 | 3 | import './SandBoxTip.css'; 4 | import data from './data'; 5 | 6 | export default function SandBoxTip() { 7 | return ( 8 | 9 | 10 | 11 | SANDBOX TIP: 12 | 13 | 14 | 15 | {data.text.tip} 16 | 17 | 18 |
19 |
20 | 21 | Institution 22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 | {data.text.usernameField} 31 | 32 | 33 | {data.text.usernameValue} 34 | 35 |
36 | 37 |
38 | 39 | Banking Password 40 | 41 | 42 | profile_02 43 | 44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/SnackBarNotification/SnackBarNotification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Snackbar, Alert } from '@mui/material'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import { snackbarActions } from '../../store/slices/snackbar'; 6 | 7 | export default function SnackBarNotificaton() { 8 | const dispatch = useDispatch(); 9 | const snackbarContent = useSelector((state: any) => state.snackbarState); 10 | let backgroundColor = '#FF5555'; 11 | if ( 12 | snackbarContent.severity === 'success' || 13 | snackbarContent.severity === 'info' 14 | ) { 15 | backgroundColor = '#2d7763'; 16 | } else if (snackbarContent.severity === 'warning') { 17 | backgroundColor = '#F79E1B'; 18 | } 19 | 20 | let position: any = { 21 | vertical: 'bottom', 22 | horizontal: 'center', 23 | }; 24 | 25 | if ( 26 | snackbarContent.severity === 'success' || 27 | snackbarContent.severity === 'info' 28 | ) { 29 | position = { 30 | vertical: 'bottom', 31 | horizontal: 'left', 32 | }; 33 | } 34 | 35 | /** 36 | * Sackbar close event handler 37 | * @param event SyntheticEvent 38 | * @param reason reason on click 39 | * @returns 40 | */ 41 | const handleClose = ( 42 | event?: React.SyntheticEvent | Event, 43 | reason?: string 44 | ) => { 45 | if (reason === 'clickaway') { 46 | return; 47 | } 48 | dispatch(snackbarActions.close()); 49 | }; 50 | return ( 51 | 58 | 69 | {snackbarContent.message} 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/tests/modal.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Modal from '../components/Modal/Modal'; 3 | import userEvent from '@testing-library/user-event'; 4 | 5 | describe('Testing Modal Component', () => { 6 | test('Should have text - Open banking', () => { 7 | render(() as React.ReactElement); 8 | expect( 9 | screen.getByText(/Open Banking/, { exact: false }) 10 | ).toBeInTheDocument(); 11 | }); 12 | 13 | test('Should have text - reference', () => { 14 | render(() as React.ReactElement); 15 | expect( 16 | screen.getByText('reference', { exact: false }) 17 | ).toBeInTheDocument(); 18 | }); 19 | 20 | test('Should have button - view demo', () => { 21 | render(() as React.ReactElement); 22 | expect( 23 | screen.getByRole('button', { name: /view demo/i }) 24 | ).toBeInTheDocument(); 25 | }); 26 | 27 | test('Should have button - view on github', () => { 28 | render(() as React.ReactElement); 29 | expect( 30 | screen.getByRole('button', { name: /view on github/i }) 31 | ).toBeInTheDocument(); 32 | }); 33 | 34 | test('view on github text should have href attribute', () => { 35 | render(() as React.ReactElement); 36 | expect( 37 | screen.getByRole('link', { name: /view on github/i }) 38 | ).toHaveAttribute('href'); 39 | }); 40 | 41 | test('view on github text should have target attribute', () => { 42 | render(() as React.ReactElement); 43 | expect( 44 | screen.getByRole('link', { name: /view on github/i }) 45 | ).toHaveAttribute('target'); 46 | }); 47 | 48 | test('view on github text should have target attribute with value _blank', () => { 49 | render(() as React.ReactElement); 50 | expect( 51 | screen.getByRole('link', { name: /view on github/i }) 52 | ).toHaveAttribute('target', '_blank'); 53 | }); 54 | 55 | test('click handleModalClose event ', async () => { 56 | const handleModalClose = async () => { 57 | return 'close'; 58 | }; 59 | render( 60 | ( 61 | 62 | ) as React.ReactElement 63 | ); 64 | await userEvent.click(screen.getByTestId('modal-close')); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/tests/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Header from '../components/Header/Header'; 3 | 4 | describe('Testing Header Component', () => { 5 | test('Should have header left content', () => { 6 | render((
) as React.ReactElement); 7 | expect( 8 | screen.getByText('developer', { exact: false }) 9 | ).toBeInTheDocument(); 10 | }); 11 | 12 | test('Should have header middle content', () => { 13 | render((
) as React.ReactElement); 14 | expect( 15 | screen.getByText('reference app', { exact: false }) 16 | ).toBeInTheDocument(); 17 | }); 18 | 19 | test('Should have header right content', () => { 20 | render((
) as React.ReactElement); 21 | expect( 22 | screen.getByText('github', { exact: false }) 23 | ).toBeInTheDocument(); 24 | }); 25 | 26 | test('developer text should have href attribute', () => { 27 | render((
) as React.ReactElement); 28 | expect( 29 | screen.getByRole('link', { name: /developers/i }) 30 | ).toHaveAttribute('href'); 31 | }); 32 | 33 | test('developer text should have target attribute', () => { 34 | render((
) as React.ReactElement); 35 | expect( 36 | screen.getByRole('link', { name: /developers/i }) 37 | ).toHaveAttribute('target'); 38 | }); 39 | 40 | test('developer text should have target attribute with value _blank', () => { 41 | render((
) as React.ReactElement); 42 | expect( 43 | screen.getByRole('link', { name: /developers/i }) 44 | ).toHaveAttribute('target', '_blank'); 45 | }); 46 | 47 | test('view on github text should have href attribute', () => { 48 | render((
) as React.ReactElement); 49 | expect( 50 | screen.getByRole('link', { name: /view on github/i }) 51 | ).toHaveAttribute('href'); 52 | }); 53 | 54 | test('view on github text should have target attribute', () => { 55 | render((
) as React.ReactElement); 56 | expect( 57 | screen.getByRole('link', { name: /view on github/i }) 58 | ).toHaveAttribute('target'); 59 | }); 60 | 61 | test('view on github text should have target attribute with value _blank', () => { 62 | render((
) as React.ReactElement); 63 | expect( 64 | screen.getByRole('link', { name: /view on github/i }) 65 | ).toHaveAttribute('target', '_blank'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/tests/ConnectForm.helper.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | activateCustomer, 3 | generateConnectUrl, 4 | createConsumer, 5 | getAccounts, 6 | } from '../components/ConnectForm/helper'; 7 | 8 | import requestData from './Mocks/request-data'; 9 | import accountsData from './Mocks/accounts-data'; 10 | 11 | describe('Testing connect form helper', () => { 12 | describe('Testing activateCustomer', () => { 13 | test('Should activate customer', async () => { 14 | window.fetch = jest.fn().mockResolvedValue({ 15 | json: async () => { 16 | return { 17 | customerId: '1234567890', 18 | }; 19 | }, 20 | status: 200, 21 | }); 22 | const result = await activateCustomer('johnsmith', requestData); 23 | expect(result.customerId).toEqual('1234567890'); 24 | }); 25 | }); 26 | 27 | describe('Testing generateConnectUrl ', () => { 28 | test('Should generate connect url', async () => { 29 | window.fetch = jest.fn().mockResolvedValue({ 30 | json: async () => { 31 | return { 32 | link: 'https://www.connecturl.com', 33 | }; 34 | }, 35 | status: 200, 36 | }); 37 | const link = await generateConnectUrl(requestData); 38 | expect(link).toEqual('https://www.connecturl.com'); 39 | }); 40 | }); 41 | 42 | describe('Testing createConsumer ', () => { 43 | test('Should create consumer ', async () => { 44 | window.fetch = jest.fn().mockResolvedValue({ 45 | json: async () => { 46 | return { 47 | consumerId: '1234567890', 48 | }; 49 | }, 50 | status: 200, 51 | }); 52 | const result = await createConsumer(requestData); 53 | expect(result.consumerId).toEqual('1234567890'); 54 | }); 55 | }); 56 | 57 | describe('Testing getAccounts ', () => { 58 | test('Should get accounts ', async () => { 59 | window.fetch = jest.fn().mockResolvedValue({ 60 | json: async () => { 61 | return { 62 | accounts: accountsData, 63 | }; 64 | }, 65 | status: 200, 66 | }); 67 | const result = await getAccounts(requestData); 68 | expect(result.accounts[0].realAccountNumberLast4).toEqual('0001'); 69 | expect(result.accounts[0].name).toEqual('Transaction'); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-banking-reference-application", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "https://api.finicity.com", 6 | "dependencies": { 7 | "@emotion/react": "^11.11.4", 8 | "@emotion/styled": "^11.11.5", 9 | "@finicity/connect-web-sdk": "^1.0.0-rc.4", 10 | "@mui/icons-material": "^5.15.19", 11 | "@mui/material": "^5.15.19", 12 | "@mui/x-date-pickers": "^7.6.2", 13 | "@reduxjs/toolkit": "^2.2.6", 14 | "@testing-library/jest-dom": "^6.4.8", 15 | "@testing-library/react": "^16.0.0", 16 | "@testing-library/user-event": "^14.5.2", 17 | "@types/jest": "^27.5.2", 18 | "@types/node": "^16.18.70", 19 | "@types/react": "^18.2.47", 20 | "@types/react-dom": "^18.2.18", 21 | "eventemitter3": "^5.0.1", 22 | "formik": "^2.1.3", 23 | "lodash": "^4.17.21", 24 | "material-react-table": "^2.13.0", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "react-json-view": "^1.21.3", 28 | "react-redux": "^9.1.2", 29 | "react-scripts": "5.0.1", 30 | "tailwindcss": "3.3.3", 31 | "typescript": "^5.4.5", 32 | "web-vitals": "^2.1.4", 33 | "yup": "^1.2.0" 34 | }, 35 | "overrides": { 36 | "react-scripts": { 37 | "typescript": "^5" 38 | }, 39 | "react-json-view": { 40 | "react": "$react", 41 | "react-dom": "$react-dom" 42 | } 43 | }, 44 | "devDependencies": { 45 | "@types/lodash": "^4.14.198", 46 | "@types/testing-library__react": "^10.2.0", 47 | "prettier": "3.0.3", 48 | "tailwindcss": "^3.3.3" 49 | }, 50 | "scripts": { 51 | "start": "react-scripts start", 52 | "build": "react-scripts build", 53 | "test": "react-scripts test", 54 | "test:coverage": "react-scripts test --env=jsdom --watchAll=false --coverage", 55 | "eject": "react-scripts eject", 56 | "lint": "eslint --fix --ext .ts,.tsx ./src", 57 | "prettier": "npx prettier --write ." 58 | }, 59 | "eslintConfig": { 60 | "extends": [ 61 | "react-app", 62 | "react-app/jest" 63 | ] 64 | }, 65 | "browserslist": { 66 | "production": [ 67 | ">0.2%", 68 | "not dead", 69 | "not op_mini all" 70 | ], 71 | "development": [ 72 | "last 1 chrome version", 73 | "last 1 firefox version", 74 | "last 1 safari version" 75 | ] 76 | }, 77 | "jest": { 78 | "coveragePathIgnorePatterns": [ 79 | "src/App.tsx", 80 | "src/index.tsx" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/ConnectForm/helper.ts: -------------------------------------------------------------------------------- 1 | import { generateFetchHeaders, handleFetchResponse } from '../../utils/helper'; 2 | 3 | import data from './data'; 4 | 5 | /** 6 | * Create a new active customer 7 | * @param userName unique username for customer 8 | * @param requestData application parameters 9 | * @returns activate customer response 10 | */ 11 | export const activateCustomer = async (userName: string, requestData: any) => { 12 | const requestHeaders = await generateFetchHeaders('POST', requestData); 13 | let body = JSON.parse(data.body.activateCustomer); 14 | body = JSON.stringify({ ...body, username: userName }); 15 | const requestOptions = { 16 | ...requestHeaders, 17 | body, 18 | }; 19 | return handleFetchResponse( 20 | await fetch(data.url.activateCustomer, requestOptions) 21 | ); 22 | }; 23 | 24 | /** 25 | * Generate connect url 26 | * @param requestData application parameters 27 | * @returns connect url 28 | */ 29 | export const generateConnectUrl = async (requestData: any) => { 30 | const requestHeaders = await generateFetchHeaders('POST', requestData); 31 | const body = JSON.stringify({ 32 | partnerId: requestData.partnerId, 33 | customerId: requestData?.customerId, 34 | }); 35 | const requestOptions = { 36 | ...requestHeaders, 37 | body, 38 | }; 39 | const { link } = await handleFetchResponse( 40 | await fetch(data.url.generateConnectUrl, requestOptions) 41 | ); 42 | return link; 43 | }; 44 | 45 | /** 46 | * Create consumer for existing customer 47 | * @param requestData application parameters 48 | * @returns create consumer response 49 | */ 50 | export const createConsumer = async (requestData: any) => { 51 | const requestHeaders = await generateFetchHeaders('POST', requestData); 52 | const requestOptions = { 53 | ...requestHeaders, 54 | body: data.body.createConsumer, 55 | }; 56 | const result = await fetch( 57 | data.url.createConsumer.replace( 58 | '', 59 | String(requestData.customerId) 60 | ), 61 | requestOptions 62 | ); 63 | return result.json(); 64 | }; 65 | 66 | /** 67 | * Get shared accounts using instituitonLoginId 68 | * @param requestData application parameters 69 | * @returns refresh accounts response 70 | */ 71 | export const getAccounts = async (requestData: any) => { 72 | const requestHeaders = await generateFetchHeaders('GET', requestData); 73 | const result = await fetch( 74 | data.url.refreshAccounts 75 | .replace('', String(requestData.customerId)) 76 | .replace( 77 | '', 78 | String(requestData.institutionLoginId) 79 | ), 80 | { ...requestHeaders } 81 | ); 82 | return result.json(); 83 | }; 84 | -------------------------------------------------------------------------------- /public/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | Toolbar, 4 | ImageListItem, 5 | Typography, 6 | Box, 7 | Button, 8 | } from '@mui/material'; 9 | 10 | import { ExternalIcon } from '../../components'; 11 | 12 | import './Header.css'; 13 | import data from './data'; 14 | 15 | export default function Header() { 16 | return ( 17 | 18 | 19 | 20 | mastercard-logo 21 | 22 | 23 | 29 | 34 | developers 35 | 36 | external-link 41 | 42 | 50 | 51 | 52 | 57 | REFERENCE APP 58 | 59 | Open Banking 60 | 61 | 67 | 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Cards/Cards.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Grid, 4 | Typography, 5 | Card, 6 | CardContent, 7 | ImageList, 8 | } from '@mui/material'; 9 | 10 | import './Cards.css'; 11 | import data from './data'; 12 | 13 | export default function Cards() { 14 | return ( 15 | 16 | 22 | {data.cardsData.map((card: any) => ( 23 | 24 | 32 | 33 | 34 | {card.title} 40 | 41 | 48 | {card.title} 49 | 50 | 54 | {card.description} 55 | 56 | 59 | 66 | {card.anchorTitle} 67 | 68 | 69 | 70 | 71 | 72 | ))} 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/components/ConnectInitiation/ConnectInitiation.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Grid, Stack, Typography } from '@mui/material'; 3 | 4 | import { SandBoxTip } from '../../components'; 5 | 6 | export default function ConnectInitiation({ 7 | user: { username, createdDate, id }, 8 | }: any) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | Customer Name 16 | 17 | 22 | John Smith 23 | 24 | 25 | 26 | 27 | 28 | 29 | Username 30 | 31 | 36 | 37 | {username} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Customer ID 46 | 47 | 52 | {id} 53 | 54 | 55 | 56 | 57 | 58 | 59 | Date created: 60 | 61 | 66 | {new Date(createdDate * 1000).toDateString()} 67 | 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/tests/utils.helper.test.tsx: -------------------------------------------------------------------------------- 1 | import { generateFetchHeaders, generateAppToken } from '../utils/helper'; 2 | 3 | import requestData from './Mocks/request-data'; 4 | 5 | describe('Testing Utils', () => { 6 | describe('Testing generateFetchHeaders', () => { 7 | test('Should test generateFetchHeaders', async () => { 8 | localStorage.setItem( 9 | 'tokenGeneratedAt', 10 | String(new Date().getTime()) 11 | ); 12 | const result = await generateFetchHeaders('GET', requestData); 13 | expect(result.method).toEqual('GET'); 14 | }); 15 | 16 | test('Should test generateFetchHeaders when requestData not provide', async () => { 17 | const result = await generateFetchHeaders('GET'); 18 | expect(result.method).toEqual('GET'); 19 | }); 20 | }); 21 | 22 | describe('Testing generateAppToken', () => { 23 | test('should test generateAppToken', async () => { 24 | window.fetch = jest.fn().mockResolvedValue({ 25 | json: async () => { 26 | return { 27 | token: 'asdfghjkl', 28 | }; 29 | }, 30 | 31 | status: 200, 32 | }); 33 | const token = await generateAppToken({ 34 | partnerId: '1234567890', 35 | partnerSecret: 'qwertyuiop', 36 | }); 37 | expect(token).toEqual('asdfghjkl'); 38 | }); 39 | 40 | test('should test generateAppToken', async () => { 41 | try { 42 | window.fetch = jest.fn().mockResolvedValue({ 43 | text: async () => 'unauthorized', 44 | 45 | status: 401, 46 | }); 47 | await generateAppToken({ 48 | partnerId: '1234567890', 49 | partnerSecret: 'qwertyuiop', 50 | }); 51 | } catch (error: any) { 52 | expect(error.message).toEqual('unauthorized'); 53 | } 54 | }); 55 | 56 | test('should test generateAppToken', async () => { 57 | try { 58 | window.fetch = jest.fn().mockResolvedValue({ 59 | text: async () => 'restricted region', 60 | 61 | status: 403, 62 | }); 63 | await generateAppToken({ 64 | partnerId: '1234567890', 65 | partnerSecret: 'qwertyuiop', 66 | }); 67 | } catch (error: any) { 68 | expect(error.message).toEqual( 69 | 'Applications accessing the Open Banking APIs must be hosted within the US.' 70 | ); 71 | } 72 | }); 73 | 74 | test('should test generateAppToken', async () => { 75 | try { 76 | window.fetch = jest.fn().mockResolvedValue({ 77 | json: async () => { 78 | return { 79 | message: 'resource not found', 80 | }; 81 | }, 82 | status: 404, 83 | }); 84 | await generateAppToken({ 85 | partnerId: '1234567890', 86 | partnerSecret: 'qwertyuiop', 87 | }); 88 | } catch (error: any) { 89 | expect(error.message).toEqual('resource not found'); 90 | } 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/tests/ConnectForm.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import ConnectForm from '../components/ConnectForm/ConnectForm'; 3 | import { Provider } from 'react-redux'; 4 | import store from '../store'; 5 | import userEvent from '@testing-library/user-event'; 6 | jest.mock('../components/ConnectForm/helper.ts'); 7 | 8 | import * as commonHelpers from '../utils/helper'; 9 | import * as helpers from '../components/ConnectForm/helper'; 10 | describe('Testing ConnectForm Component', () => { 11 | beforeEach(() => { 12 | jest.resetModules(); // Clears the cache 13 | jest.mock('../components/ConnectForm/helper'); 14 | jest.spyOn(commonHelpers, 'generateAppToken').mockImplementation(() => 15 | Promise.resolve('token_dummy') 16 | ); 17 | jest.spyOn(helpers, 'activateCustomer').mockImplementation(() => 18 | Promise.resolve({ 19 | id: '2493212', 20 | username: 'customer_1722493735', 21 | createdDate: '1722529742', 22 | }) 23 | ); 24 | jest.spyOn(helpers, 'generateConnectUrl').mockImplementation(() => 25 | Promise.resolve('connectLink') 26 | ); 27 | jest.spyOn(helpers, 'createConsumer').mockImplementation(() => 28 | Promise.resolve({ consumerId: '34455' }) 29 | ); 30 | }); 31 | afterEach(() => { 32 | jest.restoreAllMocks(); // Restores all mocks to their original implementation 33 | jest.resetAllMocks(); 34 | }); 35 | 36 | test('Should render connect form', () => { 37 | render( 38 | ( 39 | 40 | 41 | 42 | ) as React.ReactElement 43 | ); 44 | 45 | expect(screen.getByTestId('stepper')).toBeInTheDocument(); 46 | expect(screen.getByTestId('step-1')).toBeInTheDocument(); 47 | expect(screen.getByTestId('stepLabel-1')).toBeInTheDocument(); 48 | expect(screen.getByTestId('stepContent-1')).toBeInTheDocument(); 49 | expect(screen.getByTestId('form')).toBeInTheDocument(); 50 | expect(screen.getByTestId('accordian-1')).toBeInTheDocument(); 51 | expect(screen.getByTestId('accordianSummary-1')).toBeInTheDocument(); 52 | expect(screen.getByTestId('accordianDetails-1')).toBeInTheDocument(); 53 | expect(screen.getAllByTestId('customer-input')[0]).toBeInTheDocument(); 54 | }); 55 | test('trigger click event and create customer', async () => { 56 | const user = userEvent.setup(); 57 | 58 | render( 59 | ( 60 | 61 | 62 | 63 | ) as React.ReactElement 64 | ); 65 | await user.click(screen.getByRole('button', { name: /View demo/i })); 66 | expect(commonHelpers.generateAppToken).toHaveBeenCalled(); 67 | }); 68 | test('trigger click event and create customer and further steps', async () => { 69 | const user = userEvent.setup(); 70 | render( 71 | ( 72 | 73 | 74 | 75 | ) as React.ReactElement 76 | ); 77 | 78 | await user.click(screen.getByRole('button', { name: /View demo/i })); 79 | const element = await screen.findByText(/AUTO CREATION COMPLETE/i); 80 | expect(element).toBeInTheDocument(); 81 | const connectElement = await screen.findByText('Connect Bank Account'); 82 | expect(connectElement).toBeInTheDocument(); 83 | 84 | await user.click(connectElement); 85 | expect(commonHelpers.generateAppToken).toHaveBeenCalled(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/components/DataTable/DatatTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Grid, 3 | Stack, 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableContainer, 8 | TableHead, 9 | TableRow, 10 | } from '@mui/material'; 11 | import { 12 | MRT_TableBodyCellValue, 13 | MRT_TablePagination, 14 | useMaterialReactTable, 15 | } from 'material-react-table'; 16 | 17 | export default function DataTable({ columns, data, product }: any) { 18 | const table = useMaterialReactTable({ 19 | columns, 20 | data, 21 | initialState: { 22 | pagination: { 23 | pageSize: product === 'Transactions' ? 5 : 100, 24 | pageIndex: 0, 25 | }, 26 | }, 27 | muiPaginationProps: { 28 | rowsPerPageOptions: [5, 10], 29 | variant: 'outlined', 30 | }, 31 | }); 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | {table.getHeaderGroups().map((headerGroup) => ( 39 | 40 | {headerGroup.headers.map((header) => ( 41 | 50 | {header.column.columnDef.header} 51 | 52 | ))} 53 | 54 | ))} 55 | 56 | 57 | {table.getRowModel().rows.map((row, rowIndex) => ( 58 | 62 | {row.getVisibleCells().map((cell) => ( 63 | 68 | {/* Use MRT's cell renderer that provides better logic than flexRender */} 69 | 70 | 75 | 76 | ))} 77 | 78 | ))} 79 | 80 |
81 |
82 | {product === 'Transactions' && ( 83 | 84 | )} 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { APP_KEY, url, PARTNERID, PARTNERSECRET } from '../config'; 2 | 3 | /** 4 | * Get request headers for fetch call 5 | * @param method method type 6 | * @param requestData application parameters 7 | * @param accept accept type 8 | * @returns request headers 9 | */ 10 | export const generateFetchHeaders = async ( 11 | method: string, 12 | requestData: any = {}, 13 | accept = 'application/json' 14 | ) => { 15 | const myHeaders = new Headers(); 16 | myHeaders.append('Finicity-App-Key', APP_KEY ?? ''); 17 | myHeaders.append('Content-Type', 'application/json'); 18 | myHeaders.append('Accept', accept); 19 | if (requestData?.token) { 20 | if ( 21 | localStorage.getItem('tokenGeneratedAt') && 22 | new Date().getHours() - 23 | new Date( 24 | Number(localStorage.getItem('tokenGeneratedAt')) 25 | ).getHours() > 26 | 0 27 | ) { 28 | requestData.token = await generateAppToken({ 29 | partnerId: PARTNERID, 30 | partnerSecret: PARTNERSECRET, 31 | }); 32 | } 33 | myHeaders.append('Finicity-App-Token', requestData.token); 34 | } 35 | 36 | return { 37 | method: method, 38 | headers: myHeaders, 39 | mode: 'cors' as RequestMode, 40 | }; 41 | }; 42 | 43 | /** 44 | * Generate app token 45 | * @param requestData application parameters 46 | * @returns token (string) 47 | */ 48 | export const generateAppToken = async (requestData: any) => { 49 | const requestHeaders = await generateFetchHeaders('POST', requestData); 50 | const body = JSON.stringify({ 51 | partnerId: requestData.partnerId, 52 | partnerSecret: requestData?.partnerSecret, 53 | }); 54 | const requestOptions = { 55 | ...requestHeaders, 56 | body, 57 | }; 58 | const { token } = await handleFetchResponse( 59 | await fetch(url.generateAppToken, requestOptions) 60 | ); 61 | localStorage.setItem('tokenGeneratedAt', String(new Date().getTime())); 62 | return token; 63 | }; 64 | 65 | /** 66 | * Handle fetch call 67 | * @param url fetch call URL 68 | * @param requestData application parameters 69 | * @param requestHeaders request headers 70 | * @param account account 71 | * @returns fetch response 72 | */ 73 | export const handleFetchCall = async ( 74 | url: any, 75 | requestData: any, 76 | requestHeaders: any, 77 | account?: any 78 | ) => { 79 | const { institutionLoginId, customerId, startDate, endDate } = requestData; 80 | const api = url 81 | .replace('', institutionLoginId) 82 | .replace('', customerId) 83 | .replace('', account?.id) 84 | .replace('', startDate) 85 | .replace('', endDate); 86 | return handleFetchResponse(await fetch(api, { ...requestHeaders })); 87 | }; 88 | 89 | /** 90 | * Fetch response handler 91 | * @param response fetch response 92 | * @returns processed fetch response 93 | */ 94 | export const handleFetchResponse = async (response: any) => { 95 | if (response.status !== 200 && response.status !== 201) { 96 | if (response.status === 401) { 97 | const responseText = await response.text(); 98 | throw new Error(responseText); 99 | } else if (response.status === 403) { 100 | throw new Error( 101 | 'Applications accessing the Open Banking APIs must be hosted within the US.' 102 | ); 103 | } else { 104 | const { message } = await response.json(); 105 | throw new Error(message); 106 | } 107 | } 108 | return response.json(); 109 | }; 110 | -------------------------------------------------------------------------------- /src/components/ConnectForm/data/Steps.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | export const Steps = [ 3 | { 4 | label: 'Add your first customer', 5 | description: ( 6 |
7 | Start with creating a "testing" customer whose financial data 8 | will be requested. To learn more,{' '} 9 | 10 | see{' '} 11 | 16 | Customers. 17 | 18 | 19 |
20 | ), 21 | panel: 'panel0', 22 | documentationLink: 23 | 'https://developer.mastercard.com/open-banking-us/documentation/quick-start-guide/#2-welcome-your-first-test-customer', 24 | }, 25 | { 26 | label: 'Obtain access to the customer’s accounts', 27 | description: ( 28 | 29 |
30 | Having created a customer, the next step is to generate a 31 | Connect URL to launch the Connect application. 32 |
33 |
34 |
35 | The Connect application allows customers to select accounts 36 | they want to share. To learn more,{' '} 37 | 38 | see{' '} 39 | 44 | Connect Application 45 | 46 | 47 |
48 |
49 | ), 50 | panel: 'panel1', 51 | documentationLink: 52 | 'https://developer.mastercard.com/open-banking-us/documentation/connect/integrating/', 53 | }, 54 | { 55 | label: 'Pull account information', 56 | description: ( 57 |
58 |
59 | Now you can retrieve some of the latest data from the shared 60 | accounts. For that, call the{' '} 61 | 62 | 67 | Get Customer Accounts 68 | 69 | {' '} 70 | endpoint. 71 |
72 |
73 | ), 74 | panel: 'panel2', 75 | documentationLink: 76 | 'https://developer.mastercard.com/open-banking-us/documentation/api-reference/#GetCustomerAccounts', 77 | }, 78 | { 79 | label: 'Use cases', 80 | description: ( 81 |
82 | This section provides you with an overview of the different 83 | solutions offered by Mastercard Open Banking. Find out about the 84 | APIs that power these solutions and help our partners succeed. 85 |
86 | ), 87 | panel: 'panel3', 88 | documentationLink: 89 | 'https://developer.mastercard.com/open-banking-us/documentation/usecases', 90 | }, 91 | ]; 92 | 93 | export default Steps; 94 | -------------------------------------------------------------------------------- /public/finbank.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/CurlCommand/CurlCommand.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from 'react'; 2 | import { Grid, Tooltip } from '@mui/material'; 3 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 4 | 5 | import './CurlCommand.css'; 6 | import { getCurlCommandParameters } from './helper'; 7 | 8 | export default function CurlCommand({ product, requestData, body }: any) { 9 | const [curlCommandParameters, setCurlCommandParameters] = useState({}); 10 | const [curlCopyToolTip, setCurlCopyToolTip] = 11 | useState('copy to clipboard'); 12 | useEffect(() => { 13 | setCurlCommandParameters( 14 | getCurlCommandParameters(product, requestData, body) 15 | ); 16 | }, [product]); 17 | 18 | /** 19 | * copy curl command to clipboard 20 | */ 21 | const copyToClipBoard = async () => { 22 | setCurlCopyToolTip('copied!'); 23 | navigator.clipboard.writeText(curlCommandParameters.command); 24 | setTimeout(() => { 25 | setCurlCopyToolTip('copy to clipboard'); 26 | }, 2000); 27 | }; 28 | return ( 29 | 30 | 31 | 32 | 38 | 39 |
40 |
41 |                         
42 |                             
43 |                                 curl --location --request {product.requestType}{' '}
44 |                                 \
45 |                             
46 |                             
47 | 48 | {curlCommandParameters.url} \ 49 | {' '} 50 |
51 | {curlCommandParameters?.headers?.map( 52 | (header: any, index: number, arr: string[]) => ( 53 | 54 | 55 | --header{' '} 56 | 57 | '{header.key}: {header.value}' \ 58 | 59 | 60 | {index < arr.length - 1 &&
} 61 |
62 | ) 63 | )} 64 | {curlCommandParameters.dataRaw && 65 | Object.keys(curlCommandParameters?.dataRaw) 66 | .length > 0 && ( 67 | 68 |
69 | --data-raw{' '} 70 | 71 | {JSON.stringify( 72 | curlCommandParameters.dataRaw, 73 | null, 74 | 2 75 | )} 76 | 77 |
78 | )}{' '} 79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Usecases/Usecases.tsx: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent, useEffect, useState } from 'react'; 2 | import { Grid, Stack, Tabs, Tab } from '@mui/material'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | 5 | import { SnackBarNotification } from '../../components'; 6 | import { Lend, Manage, Pay } from './components'; 7 | 8 | import data from './data'; 9 | import { snackbarActions } from '../../store/slices/snackbar'; 10 | 11 | export default function Usecases({ requestData }: any) { 12 | const dispatch = useDispatch(); 13 | const reportGenerationProgress = useSelector( 14 | (state: any) => state.reportProgress.progress 15 | ); 16 | const [canChangeTab, setCanChangeTab] = useState(true); 17 | const [currentUsecase, setCurrentUsecase] = useState('Lend'); 18 | const a11yProps = (index: number) => { 19 | return { 20 | id: `simple-tab-${index}`, 21 | 'aria-controls': `simple-tabpanel-${index}`, 22 | }; 23 | }; 24 | 25 | useEffect(() => { 26 | setCanChangeTab(reportGenerationProgress === 0); 27 | }, [reportGenerationProgress]); 28 | 29 | /** 30 | * Change tab handler 31 | * @param event SyntheticEvent 32 | * @param newValue new tab value 33 | */ 34 | const handleReportChangeTabs = ( 35 | event: SyntheticEvent, 36 | newValue: string 37 | ) => { 38 | if (canChangeTab) { 39 | setCurrentUsecase( 40 | data.usecases.find((report: any) => newValue === report) 41 | ); 42 | } else { 43 | dispatch( 44 | snackbarActions.open({ 45 | message: data.text.waitForReport, 46 | severity: 'warning', 47 | timeout: 2000, 48 | }) 49 | ); 50 | } 51 | }; 52 | return ( 53 | 58 | 59 | 60 | 66 | {data.usecases.map((usecase: any, index: number) => ( 67 | 74 | ))} 75 | 76 |
77 | 78 | {currentUsecase === 'Lend' && 79 | 'Make confident lending decisions and offer a hassle-free lending experience for your customers. You can generate below mentioned reports for your connected accounts.'} 80 | {currentUsecase === 'Manage' && 81 | 'Provide a consolidated view of your customers’ finances in a single space to help your customers manage their wealth better.'} 82 | {currentUsecase === 'Pay' && 83 | 'Provide a seamless payment experience for your customers.'} 84 | 85 |
86 | {currentUsecase === 'Lend' && ( 87 | 88 | )} 89 | {currentUsecase === 'Manage' && ( 90 | 91 | )} 92 | {currentUsecase === 'Pay' && } 93 |
94 | 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Box, Button, Dialog, Link, Stack, Typography } from '@mui/material'; 3 | 4 | import { ExternalIcon } from '../../components'; 5 | 6 | import './Modal.css'; 7 | import data from './data'; 8 | 9 | import { PARTNERID, PARTNERSECRET, APP_KEY } from '../../config'; 10 | 11 | export default function Modal({ handleModalClose }: any) { 12 | const [open, setOpen] = useState(!!(PARTNERID && PARTNERSECRET && APP_KEY)); 13 | 14 | /** 15 | * Handle close event handler for modal close 16 | */ 17 | const handleClose = async () => { 18 | setOpen(false); 19 | if (handleModalClose) { 20 | await handleModalClose(); 21 | } 22 | }; 23 | 24 | return ( 25 | 31 | 32 | 33 | 34 | Open Banking{' '} 39 | developers 40 | 41 | 42 | 43 | REFERENCE APP 44 | 45 | 46 | Open Banking 47 | 48 | 49 | {data.text.description} 50 | 51 | 57 | 65 | {data.text.more} 66 | 67 | product-img 72 | 73 | 74 | 75 | 76 | 84 | 89 | 101 | 102 | 103 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/tests/SnackBarNotification.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import SnackBarNotificaton from '../components/SnackBarNotification/SnackBarNotification'; 3 | import { Provider } from 'react-redux'; 4 | import { configureStore } from '@reduxjs/toolkit'; 5 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 6 | 7 | describe('Testing Snack Bar Component', () => { 8 | beforeEach(() => { 9 | jest.restoreAllMocks(); // Restores all mocks to their original implementation 10 | jest.resetAllMocks(); 11 | }); 12 | test('Should open snack bar when severity error', () => { 13 | const snackBarSlice = createSlice({ 14 | name: 'reportProgress', 15 | initialState: { 16 | open: true, 17 | message: 'Something went wrong', 18 | severity: 'error', 19 | timeout: 5000, 20 | }, 21 | reducers: { 22 | open(state: any, action: PayloadAction) { 23 | state.open = true; 24 | state.severity = action.payload.severity || state.severity; 25 | state.message = 26 | action.payload.message || 27 | (state.severity === 'error' 28 | ? 'Something went wrong' 29 | : state.message); 30 | state.timeout = action.payload.timeout || state.timeout; 31 | }, 32 | close(state: any) { 33 | state.open = false; 34 | }, 35 | }, 36 | }); 37 | const mockStore = configureStore({ 38 | reducer: { snackbarState: snackBarSlice.reducer }, 39 | }); 40 | render( 41 | ( 42 | 43 | 44 | 45 | ) as React.ReactElement 46 | ); 47 | const element = screen.queryByTestId('snackbar-message'); 48 | expect(element?.textContent).toEqual('Something went wrong'); 49 | }); 50 | 51 | test('Should open snack bar when severity sucess', () => { 52 | const snackBarSlice = createSlice({ 53 | name: 'reportProgress', 54 | initialState: { 55 | open: true, 56 | message: 'Something went wrong', 57 | severity: 'success', 58 | timeout: 5000, 59 | }, 60 | reducers: { 61 | open(state: any, action: PayloadAction) { 62 | state.open = true; 63 | state.severity = action.payload.severity || state.severity; 64 | state.message = 65 | action.payload.message || 66 | (state.severity === 'error' 67 | ? 'Something went wrong' 68 | : state.message); 69 | state.timeout = action.payload.timeout || state.timeout; 70 | }, 71 | close(state: any) { 72 | state.open = false; 73 | }, 74 | }, 75 | }); 76 | const mockStore = configureStore({ 77 | reducer: { snackbarState: snackBarSlice.reducer }, 78 | }); 79 | render( 80 | ( 81 | 82 | 83 | 84 | ) as React.ReactElement 85 | ); 86 | const element = screen.queryByTestId('snackbar-message'); 87 | expect(element?.textContent).toEqual('Something went wrong'); 88 | }); 89 | 90 | test('Should open snack bar when severity warning', async () => { 91 | const snackBarSlice = createSlice({ 92 | name: 'reportProgress', 93 | initialState: { 94 | open: true, 95 | message: 'Something went wrong', 96 | severity: 'warning', 97 | timeout: 1, 98 | }, 99 | reducers: { 100 | open(state: any, action: PayloadAction) { 101 | state.open = true; 102 | state.severity = action.payload.severity || state.severity; 103 | state.message = 104 | action.payload.message || 105 | (state.severity === 'error' 106 | ? 'Something went wrong' 107 | : state.message); 108 | state.timeout = action.payload.timeout || state.timeout; 109 | }, 110 | close(state: any) { 111 | state.open = false; 112 | }, 113 | }, 114 | }); 115 | const mockStore = configureStore({ 116 | reducer: { snackbarState: snackBarSlice.reducer }, 117 | }); 118 | render( 119 | ( 120 | 121 | 122 | 123 | ) as React.ReactElement 124 | ); 125 | const element = screen.queryByTestId('snackbar-message'); 126 | expect(element?.textContent).toEqual('Something went wrong'); 127 | await screen.findByText('Something went wrong', {}, { timeout: 30 }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Lend/data/index.ts: -------------------------------------------------------------------------------- 1 | const requestBody = { 2 | reportCustomFields: [ 3 | { 4 | label: 'loanID', 5 | value: 123456, 6 | shown: true, 7 | }, 8 | { 9 | label: 'loanID', 10 | value: 123456, 11 | shown: true, 12 | }, 13 | ], 14 | incomeStreamConfidenceMinimum: 50, 15 | }; 16 | 17 | const requestBodyWithPayroll = { 18 | ...requestBody, 19 | payrollData: { 20 | ssn: '999601111', 21 | dob: 15983999, 22 | }, 23 | }; 24 | 25 | const data = { 26 | text: { 27 | accountError: 28 | 'None of the shared accounts are supported for selected report type.', 29 | waitForReport: 'Please wait while the report is generating.', 30 | }, 31 | reports: [ 32 | { 33 | name: 'Verification of Assets', 34 | requestType: 'POST', 35 | identifier: 'voa', 36 | shortName: 'VOA', 37 | api: '/decisioning/v2/customers//voa', 38 | body: JSON.stringify(requestBody), 39 | }, 40 | { 41 | name: 'Verification of Income', 42 | requestType: 'POST', 43 | identifier: 'voi', 44 | shortName: 'VOI', 45 | api: '/decisioning/v2/customers//voi', 46 | body: JSON.stringify(requestBody), 47 | }, 48 | { 49 | name: 'Verification of Assets with Income - Transactions', 50 | requestType: 'POST', 51 | identifier: 'voait', 52 | shortName: 'Verification of Assets with Income - Transactions', 53 | api: '/decisioning/v2/customers//voaHistory', 54 | body: JSON.stringify(requestBody), 55 | }, 56 | { 57 | name: 'Verification of Income and Employment - Payroll', 58 | requestType: 'POST', 59 | identifier: 'voiep', 60 | shortName: 'Verification of Income and Employment - Payroll', 61 | api: '/decisioning/v2/customers//voiePayroll', 62 | body: JSON.stringify(requestBodyWithPayroll), 63 | }, 64 | { 65 | name: 'Verification of Income and Employment - Paystub', 66 | requestType: 'POST', 67 | identifier: 'voieptx', 68 | shortName: 'Verification of Income and Employment - Paystub', 69 | api: '/decisioning/v2/customers//voieTxVerify/withInterview', 70 | body: JSON.stringify(requestBody), 71 | }, 72 | { 73 | name: 'Verification of Employment - Payroll', 74 | requestType: 'POST', 75 | identifier: 'voep', 76 | shortName: 'Verification of Employment - Payroll', 77 | api: '/decisioning/v2/customers//voePayroll', 78 | body: JSON.stringify(requestBodyWithPayroll), 79 | }, 80 | { 81 | name: 'Verification of Employment - Transactions', 82 | requestType: 'POST', 83 | identifier: 'voet', 84 | shortName: 'Verification of Employment - Transactions', 85 | api: '/decisioning/v2/customers//voeTransactions', 86 | body: JSON.stringify(requestBody), 87 | }, 88 | { 89 | name: 'Prequalification CRA Report', 90 | requestType: 'POST', 91 | identifier: 'pcra', 92 | shortName: 'Prequalification Non-CRA Report', 93 | api: '/decisioning/v2/customers//preQualVoa', 94 | body: JSON.stringify(requestBody), 95 | }, 96 | { 97 | name: 'Prequalification Non-CRA Report', 98 | requestType: 'POST', 99 | identifier: 'pncra', 100 | shortName: 'Prequalification Non-CRA Report', 101 | api: '/decisioning/v2/customers//assetSummary', 102 | body: JSON.stringify(requestBody), 103 | }, 104 | { 105 | name: 'Transaction Credit Reporting Agency', 106 | requestType: 'POST', 107 | identifier: 'tcra', 108 | shortName: 'Transaction Credit Reporting Agency', 109 | api: '/decisioning/v2/customers//transactions', 110 | body: JSON.stringify(requestBody), 111 | }, 112 | { 113 | name: 'Cash Flow Report', 114 | requestType: 'POST', 115 | identifier: 'cfr', 116 | shortName: 'cash flow', 117 | api: '/decisioning/v2/customers//cashFlowBusiness', 118 | body: JSON.stringify(requestBody), 119 | }, 120 | { 121 | name: 'Balance Analytics Report', 122 | requestType: 'POST', 123 | identifier: 'bar', 124 | shortName: 'balance analytics', 125 | api: '/analytics/balance/v1/customer/', 126 | body: JSON.stringify({}), 127 | }, 128 | { 129 | name: 'Cash Flow Analytics Report', 130 | requestType: 'POST', 131 | identifier: 'cfar', 132 | shortName: 'cash flow analytics', 133 | api: '/analytics/cashflow/v1/customer/', 134 | body: JSON.stringify({}), 135 | }, 136 | ], 137 | url: { 138 | getReportStatus: '/decisioning/v1/customers//reports', 139 | getReport: 140 | '/decisioning/v4/customers//reports/?onBehalfOf=Some entity&purpose=99', 141 | getAnalyticsReport: '/analytics/data/v1/', 142 | payStatements: '/aggregation/v1/customers//payStatements', 143 | }, 144 | }; 145 | 146 | export default data; 147 | -------------------------------------------------------------------------------- /src/tests/Lend.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, render, screen, fireEvent } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { Lend } from '../components/Usecases/components'; 5 | import data from '../components/Usecases/components/Lend/data'; 6 | 7 | import store from '../store'; 8 | 9 | import requestData from './Mocks/request-data'; 10 | import userEvent from '@testing-library/user-event'; 11 | 12 | describe('Testing manage component', () => { 13 | beforeEach(() => { 14 | jest.resetModules(); // Clears the cache 15 | }); 16 | afterEach(() => { 17 | jest.restoreAllMocks(); // Restores all mocks to their original implementation 18 | jest.resetAllMocks(); 19 | }); 20 | 21 | test('should render manage component', () => { 22 | render( 23 | 24 | 25 | 26 | ); 27 | expect(screen.getByTestId('lend')).toBeInTheDocument(); 28 | }); 29 | 30 | test('should fetch report', async () => { 31 | window.fetch = jest.fn().mockResolvedValue({ 32 | json: async () => { 33 | return { 34 | id: '123', 35 | reportId: ' qwertyuiop', 36 | status: 'success', 37 | reports: [ 38 | { 39 | id: 'qwertyuiop', 40 | reportId: ' qwertyuiop', 41 | status: 'success', 42 | }, 43 | ], 44 | }; 45 | }, 46 | blob: async () => { 47 | return 'data'; 48 | }, 49 | status: 200, 50 | }); 51 | render( 52 | 53 | 54 | 55 | ); 56 | const generateReportElement = 57 | await screen.findByTestId('generate-report'); 58 | expect(generateReportElement).toBeInTheDocument(); 59 | await act(() => fireEvent.click(generateReportElement)); 60 | }); 61 | 62 | test('should fetch paystub', async () => { 63 | window.fetch = jest.fn().mockResolvedValue({ 64 | json: async () => { 65 | return { 66 | id: 'qwertyuiop', 67 | reportId: ' qwertyuiop', 68 | status: 'success', 69 | assetId: '12345678', 70 | reports: [ 71 | { 72 | id: 'qwertyuiop', 73 | reportId: ' qwertyuiop', 74 | status: 'success', 75 | }, 76 | ], 77 | }; 78 | }, 79 | blob: async () => { 80 | return 'data'; 81 | }, 82 | status: 200, 83 | }); 84 | render( 85 | 86 | 87 | 88 | ); 89 | const selectElement = screen.getByRole('combobox'); 90 | await act(() => userEvent.click(selectElement)); 91 | const selectElementoption = await screen.findByText( 92 | 'Verification of Income and Employment - Paystub' 93 | ); 94 | await act(() => userEvent.click(selectElementoption)); 95 | const generateReportElement = 96 | await screen.findByTestId('generate-report'); 97 | expect(generateReportElement).toBeInTheDocument(); 98 | await act(() => fireEvent.click(generateReportElement)); 99 | }); 100 | 101 | test('should fetch analytics', async () => { 102 | window.fetch = jest.fn().mockResolvedValue({ 103 | json: async () => { 104 | return { 105 | id: 'qwertyuiop', 106 | reportId: ' qwertyuiop', 107 | status: 'success', 108 | reports: [ 109 | { 110 | id: 'qwertyuiop', 111 | reportId: ' qwertyuiop', 112 | status: 'success', 113 | }, 114 | ], 115 | }; 116 | }, 117 | blob: async () => { 118 | return 'data'; 119 | }, 120 | status: 200, 121 | }); 122 | render( 123 | 124 | 125 | 126 | ); 127 | const selectElement = screen.getByRole('combobox'); 128 | await act(() => userEvent.click(selectElement)); 129 | 130 | const selectElementoption = await screen.findByText( 131 | data.reports[12].name 132 | ); 133 | await act(() => userEvent.click(selectElementoption)); 134 | const generateReportElement = 135 | await screen.findByTestId('generate-report'); 136 | expect(generateReportElement).toBeInTheDocument(); 137 | await act(() => fireEvent.click(generateReportElement)); 138 | }); 139 | 140 | test('should download json report', async () => { 141 | render( 142 | 143 | 144 | 145 | ); 146 | const generateReportElement = 147 | await screen.findByTestId('json-download'); 148 | await act(() => fireEvent.click(generateReportElement)); 149 | expect(screen.getByTestId('json-download')).toBeInTheDocument(); 150 | }); 151 | 152 | test('should download pdf report', async () => { 153 | window.URL.createObjectURL = jest.fn().mockReturnValueOnce('data'); 154 | render( 155 | 156 | 157 | 158 | ); 159 | const generateReportElement = await screen.findByTestId('pdf-download'); 160 | await act(() => fireEvent.click(generateReportElement)); 161 | expect(screen.getByTestId('pdf-download')).toBeInTheDocument(); 162 | }); 163 | 164 | test('should download pdf report', async () => { 165 | window.URL.createObjectURL = jest.fn().mockReturnValueOnce('data'); 166 | render( 167 | 168 | 169 | 170 | ); 171 | 172 | const selectElement = screen.getByRole('combobox'); 173 | await act(() => userEvent.click(selectElement)); 174 | 175 | const selectElementoption = await screen.findByText( 176 | data.reports[1].name 177 | ); 178 | await act(() => userEvent.click(selectElementoption)); 179 | 180 | await screen.findByText('Generate report'); 181 | expect( 182 | screen.getByRole('button', { 183 | name: 'Generate report', 184 | }) 185 | ).toBeInTheDocument(); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /src/tests/Product.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent, act } from '@testing-library/react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { Product } from '../components'; 5 | import store from '../store'; 6 | 7 | import requestData from './Mocks/request-data'; 8 | import accounts from './Mocks/accounts-data'; 9 | import userEvent from '@testing-library/user-event'; 10 | 11 | describe('Testing product compoment', () => { 12 | beforeEach(() => { 13 | jest.resetModules(); // Clears the cache 14 | }); 15 | 16 | afterEach(() => { 17 | jest.restoreAllMocks(); // Restores all mocks to their original implementation 18 | jest.resetAllMocks(); 19 | }); 20 | test('Should render product name', () => { 21 | render( 22 | 23 | 27 | 28 | ); 29 | const requestTypeElement = screen.queryByTestId('request-type'); 30 | expect(requestTypeElement).toBeInTheDocument(); 31 | expect(requestTypeElement?.textContent).toEqual('GET'); 32 | }); 33 | 34 | test('Should render product request type', () => { 35 | render( 36 | 37 | 41 | 42 | ); 43 | const productNameElement = screen.queryByTestId('product-name'); 44 | expect(productNameElement).toBeInTheDocument(); 45 | expect(productNameElement?.textContent).toEqual('Account ACH Details'); 46 | }); 47 | 48 | test('Should render curl command', async () => { 49 | render( 50 | 51 | 55 | 56 | ); 57 | await fireEvent.click(screen.getByTestId('button-show-request')); 58 | const reportSelectElement = await screen.findByTestId('curl-command'); 59 | expect(reportSelectElement).toBeInTheDocument(); 60 | }); 61 | 62 | test('Should fetch account information', async () => { 63 | window.fetch = jest.fn().mockResolvedValue({ 64 | json: async () => { 65 | return { 66 | accounts, 67 | }; 68 | }, 69 | status: 200, 70 | }); 71 | render( 72 | 73 | 74 | 75 | ); 76 | await act(() => fireEvent.click(screen.getByTestId('send-request'))); 77 | const requestTypeElement = screen.queryByTestId('request-type'); 78 | expect(requestTypeElement).toBeInTheDocument(); 79 | expect(requestTypeElement?.textContent).toEqual('GET'); 80 | }); 81 | 82 | test('Should refresh shared accounts', async () => { 83 | window.fetch = jest.fn().mockResolvedValueOnce({ 84 | json: async () => { 85 | return { 86 | accounts, 87 | }; 88 | }, 89 | status: 200, 90 | }); 91 | render( 92 | 93 | 97 | 98 | ); 99 | await act(() => fireEvent.click(screen.getByTestId('send-request'))); 100 | const requestTypeElement = screen.queryByTestId('request-type'); 101 | expect(requestTypeElement).toBeInTheDocument(); 102 | expect(requestTypeElement?.textContent).toEqual('POST'); 103 | }); 104 | 105 | test('Should refresh shared accounts - failed', async () => { 106 | window.fetch = jest.fn().mockResolvedValueOnce({ 107 | json: async () => { 108 | throw new Error('test failed'); 109 | }, 110 | status: 200, 111 | }); 112 | render( 113 | 114 | 118 | 119 | ); 120 | await act(() => fireEvent.click(screen.getByTestId('send-request'))); 121 | const requestTypeElement = screen.queryByTestId('request-type'); 122 | expect(requestTypeElement).toBeInTheDocument(); 123 | expect(requestTypeElement?.textContent).toEqual('POST'); 124 | }); 125 | 126 | test('Should toggle display data', async () => { 127 | window.fetch = jest.fn().mockResolvedValue({ 128 | json: async () => { 129 | return { 130 | accounts, 131 | }; 132 | }, 133 | status: 200, 134 | }); 135 | 136 | render( 137 | 138 | 139 | 140 | ); 141 | await act(() => fireEvent.click(screen.getByTestId('send-request'))); 142 | await act(() => fireEvent.click(screen.getByTestId('json'))); 143 | const requestTypeElement = screen.queryByTestId('request-type'); 144 | expect(requestTypeElement).toBeInTheDocument(); 145 | expect(requestTypeElement?.textContent).toEqual('GET'); 146 | }); 147 | 148 | test('Should render product with account drop down', async () => { 149 | render( 150 | ( 151 | 152 | 156 | 157 | ) as React.ReactElement 158 | ); 159 | const reportSelectElement = screen.queryByTestId('account-select'); 160 | expect(reportSelectElement).toBeInTheDocument(); 161 | 162 | let selectElement = screen.getByText( 163 | requestData.accountDisplayNames[0] 164 | ); 165 | await act(() => userEvent.click(selectElement)); 166 | 167 | let selectElementoption = await screen.findByText( 168 | requestData.accountDisplayNames[1] 169 | ); 170 | await act(() => userEvent.click(selectElementoption)); 171 | 172 | await screen.findByText(requestData.accountDisplayNames[1]); 173 | 174 | selectElement = screen.getByText(requestData.accountDisplayNames[1]); 175 | await act(() => userEvent.click(selectElement)); 176 | 177 | selectElementoption = await screen.findByText( 178 | requestData.accountDisplayNames[2] 179 | ); 180 | await act(() => userEvent.click(selectElementoption)); 181 | 182 | await screen.findByText(requestData.accountDisplayNames[2]); 183 | expect(reportSelectElement).toBeInTheDocument(); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Lend/Lend.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react'; 2 | import { 3 | Grid, 4 | Stack, 5 | Select, 6 | MenuItem, 7 | SelectChangeEvent, 8 | Button, 9 | Tooltip, 10 | } from '@mui/material'; 11 | import { useSelector, useDispatch } from 'react-redux'; 12 | 13 | import { reportProgressActions } from '../../../../store/slices/report-progress'; 14 | import { 15 | CircularProgressWithValue, 16 | Product, 17 | SnackBarNotification, 18 | } from '../../../../components'; 19 | import { snackbarActions } from '../../../../store/slices/snackbar'; 20 | 21 | import './Lend.css'; 22 | import data from './data'; 23 | import { submitReport, downloadReport } from './helper'; 24 | 25 | export default function Lend({ requestData }: any) { 26 | const dispatch = useDispatch(); 27 | const reportGenerationProgress = useSelector( 28 | (state: any) => state.reportProgress.progress 29 | ); 30 | const [currentReport, setCurrentReport] = useState(data.reports[0]); 31 | const [disableGenerateReport, setDisableGenerateReport] = 32 | useState(false); 33 | 34 | /** 35 | * Report select change event handler 36 | * @param event SelectChangeEvent 37 | */ 38 | const handleReportChangeSelect = (event: SelectChangeEvent) => { 39 | const newReport: any = data.reports.find( 40 | (report: any) => event.target.value === report.name 41 | ); 42 | setCurrentReport(newReport); 43 | }; 44 | 45 | /** 46 | * Generate report 47 | */ 48 | const generateReport = async () => { 49 | setDisableGenerateReport(true); 50 | try { 51 | const report = await submitReport( 52 | currentReport, 53 | requestData, 54 | dispatch 55 | ); 56 | currentReport['pdf'] = report?.pdf; 57 | currentReport['json'] = report?.json; 58 | const newReport = currentReport; 59 | setCurrentReport(newReport); 60 | } catch (error: any) { 61 | if (error.message) { 62 | dispatch( 63 | snackbarActions.open({ 64 | message: error.message, 65 | severity: error.cause ? 'warning' : 'error', 66 | }) 67 | ); 68 | } 69 | } 70 | setDisableGenerateReport(false); 71 | dispatch(reportProgressActions.absoluteValue(0)); 72 | }; 73 | 74 | /** 75 | * Downlod JSON report 76 | * @param event event 77 | */ 78 | const downloadJsonReport = async (event: any) => { 79 | event.preventDefault(); 80 | downloadReport(currentReport.json, currentReport.name, false); 81 | }; 82 | 83 | /** 84 | * Downlod PDF report 85 | * @param event event 86 | */ 87 | const downloadPdfReport = async (event: any) => { 88 | event.preventDefault(); 89 | downloadReport(currentReport.pdf, currentReport.name); 90 | }; 91 | 92 | return ( 93 | 94 | 95 | 96 |
97 | 98 | 105 | 122 | 123 | 124 |
125 | 130 | 131 | 132 | {!currentReport.json && ( 133 | 151 | )} 152 | {currentReport.json && ( 153 | 161 | )} 162 | {currentReport.pdf && ( 163 | 171 | )} 172 | 173 | 174 |
175 | 176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /src/components/Usecases/components/Lend/helper.ts: -------------------------------------------------------------------------------- 1 | import data from './data'; 2 | import { statement } from './data/statement'; 3 | 4 | import { generateFetchHeaders } from '../../../../utils/helper'; 5 | import { Dispatch } from 'redux'; 6 | import { reportProgressActions } from '../../../../store/slices/report-progress'; 7 | 8 | /** 9 | * Submit report for generation 10 | * @param reportData report data 11 | * @param requestData application parameters 12 | * @param dispatch redux action dispatcher 13 | * @returns 14 | */ 15 | export const submitReport = async ( 16 | reportData: any, 17 | requestData: any, 18 | dispatch: Dispatch 19 | ) => { 20 | const isAnalyticsReports = ['bar', 'cfar'].includes(reportData.identifier); 21 | dispatch( 22 | reportProgressActions.increaseByvalue(isAnalyticsReports ? 50 : 5) 23 | ); 24 | const reportId = await generateReport(reportData, requestData); 25 | dispatch( 26 | reportProgressActions.increaseByvalue(isAnalyticsReports ? 25 : 10) 27 | ); 28 | if ( 29 | await reportGenerated( 30 | reportId, 31 | requestData, 32 | dispatch, 33 | isAnalyticsReports, 34 | reportData 35 | ) 36 | ) { 37 | dispatch(reportProgressActions.absoluteValue(90)); 38 | return getReport(reportId, requestData, isAnalyticsReports); 39 | } 40 | }; 41 | 42 | /** 43 | * Generate report 44 | * @param reportData any 45 | * @param requestData application parameters 46 | * @returns generate report response 47 | */ 48 | const generateReport = async (reportData: any, requestData: any) => { 49 | let requestBody = reportData.body; 50 | const requestHeaders = await generateFetchHeaders('POST', requestData); 51 | if (reportData.identifier === 'voieptx') { 52 | const assetId = await storeCustomerPayStatement( 53 | requestHeaders, 54 | requestData 55 | ); 56 | const updatedBody = { 57 | voieWithInterviewData: { 58 | txVerifyInterview: [ 59 | { 60 | assetId: assetId, 61 | }, 62 | ], 63 | extractEarnings: true, 64 | extractDeductions: true, 65 | extractDirectDeposit: true, 66 | }, 67 | reportCustomFields: [ 68 | { 69 | label: 'loanID', 70 | value: '123456', 71 | shown: true, 72 | }, 73 | { 74 | label: 'loanID', 75 | value: '123456', 76 | shown: true, 77 | }, 78 | ], 79 | }; 80 | requestBody = JSON.stringify(updatedBody); 81 | } 82 | const requestOptions = { ...requestHeaders, body: requestBody }; 83 | const result = await fetch( 84 | reportData.api.replace('', requestData.customerId), 85 | requestOptions 86 | ); 87 | const reportResult = await result.json(); 88 | return reportResult?.id || reportResult?.reportId; 89 | }; 90 | 91 | const storeCustomerPayStatement = async ( 92 | requestHeaders: any, 93 | requestData: any 94 | ) => { 95 | const requestOptions = { 96 | ...requestHeaders, 97 | body: JSON.stringify({ 98 | label: 'lastPayPeriod', 99 | statement, 100 | }), 101 | }; 102 | const result = await fetch( 103 | data.url.payStatements.replace('', requestData.customerId), 104 | requestOptions 105 | ); 106 | const payStatementResponse = await result.json(); 107 | return payStatementResponse.assetId; 108 | }; 109 | 110 | /** 111 | * Delay 112 | * @param ms delay time in milliseconds 113 | * @returns Promise with setTimeout 114 | */ 115 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 116 | 117 | /** 118 | * Check if report generated or not 119 | * @param reportId submitted report id 120 | * @param requestData application parameters 121 | * @param dispatch redux action dispatcher 122 | * @returns report generated or not 123 | */ 124 | const reportGenerated = async ( 125 | reportId: string, 126 | requestData: any, 127 | dispatch: Dispatch, 128 | isAnalytics: boolean, 129 | reportData: any 130 | ) => { 131 | if (isAnalytics) return true; 132 | let retry = 10; 133 | let reportsPending = await checkReportStatus( 134 | reportId, 135 | requestData, 136 | dispatch 137 | ); 138 | if (!reportsPending) { 139 | return true; 140 | } 141 | while (retry > 0) { 142 | retry--; 143 | await delay(reportData.identifier === 'voieptx' ? 20000 : 2000); 144 | reportsPending = await checkReportStatus( 145 | reportId, 146 | requestData, 147 | dispatch 148 | ); 149 | if (!reportsPending) { 150 | break; 151 | } 152 | } 153 | return retry > 0; 154 | }; 155 | 156 | /** 157 | * Check report status 158 | * @param reportId submitted report id 159 | * @param requestData application parameters 160 | * @param dispatch redux action dispatcher 161 | * @returns report generated or not 162 | */ 163 | const checkReportStatus = async ( 164 | reportId: string, 165 | requestData: any, 166 | dispatch: Dispatch 167 | ) => { 168 | const requestHeaders = await generateFetchHeaders('GET', requestData); 169 | const result = await fetch( 170 | data.url.getReportStatus.replace( 171 | '', 172 | String(requestData.customerId) 173 | ), 174 | { ...requestHeaders } 175 | ); 176 | const reportsStatusResult = await result.json(); 177 | const pendingReports = reportsStatusResult?.reports 178 | .filter((report: any) => report?.id === reportId) 179 | .filter((report: any) => report?.status != 'success'); 180 | dispatch(reportProgressActions.increaseByvalue(5)); 181 | return pendingReports.length > 0; 182 | }; 183 | 184 | /** 185 | * Get report 186 | * @param reportId submitted report id 187 | * @param requestData application parameters 188 | * @returns get report (json and pdf) 189 | */ 190 | const getReport = async ( 191 | reportId: string, 192 | requestData: any, 193 | isAnalytics: boolean 194 | ) => { 195 | const requestHeadersPDF = await generateFetchHeaders( 196 | 'GET', 197 | requestData, 198 | 'application/pdf' 199 | ); 200 | const requestHeadersJSON = await generateFetchHeaders('GET', requestData); 201 | const url = isAnalytics ? data.url.getAnalyticsReport : data.url.getReport; 202 | const getReports = [ 203 | fetch( 204 | url 205 | .replace('', String(requestData.customerId)) 206 | .replace('', reportId), 207 | { ...requestHeadersPDF } 208 | ), 209 | fetch( 210 | url 211 | .replace('', String(requestData.customerId)) 212 | .replace('', reportId), 213 | { ...requestHeadersJSON } 214 | ), 215 | ]; 216 | const reports = await Promise.all(getReports); 217 | const blob = await reports[0].blob(); 218 | const jsonReport = await reports[1].json(); 219 | return { pdf: blob, json: jsonReport }; 220 | }; 221 | 222 | /** 223 | * Download report 224 | * @param report generated report 225 | * @param filename filename 226 | * @param isPdf is PDF or not 227 | */ 228 | export const downloadReport = (report: any, filename: string, isPdf = true) => { 229 | const fileURL = isPdf 230 | ? window.URL.createObjectURL(report) 231 | : `data:text/json;charset=utf-8,${encodeURIComponent( 232 | JSON.stringify(report, null, 2) 233 | )}`; 234 | const alink = document.createElement('a'); 235 | alink.href = fileURL; 236 | alink.download = `${filename}.${isPdf ? 'pdf' : 'json'}`; 237 | alink.click(); 238 | }; 239 | -------------------------------------------------------------------------------- /src/tests/product.helper.test.tsx: -------------------------------------------------------------------------------- 1 | import { proccessSendRequest } from '../components/Product/helper'; 2 | import data from '../components/Product/data/index'; 3 | 4 | import accounts from './Mocks/accounts-data'; 5 | 6 | describe('getAccountsInformation', () => { 7 | test('should return tableData and response when identifier is accounts', async () => { 8 | const product = data.products[0]; 9 | const requestData = { userId: 123 }; 10 | 11 | window.fetch = jest.fn().mockResolvedValue({ 12 | json: async () => { 13 | return { 14 | accounts, 15 | }; 16 | }, 17 | status: 200, 18 | }); 19 | const result = await proccessSendRequest(product, requestData); 20 | expect(result.tableData[0].id).toEqual('15415575'); 21 | }); 22 | 23 | test('should return tableData and response when identifier is refresh-accounts', async () => { 24 | const product = data.products[1]; 25 | const requestData = { userId: 123 }; 26 | 27 | window.fetch = jest.fn().mockResolvedValue({ 28 | json: async () => { 29 | return { 30 | accounts, 31 | }; 32 | }, 33 | status: 200, 34 | }); 35 | const result = await proccessSendRequest(product, requestData); 36 | expect(result.accounts.length).toEqual(6); 37 | }); 38 | 39 | test('should return tableData and response when identifier is transactions', async () => { 40 | const product = data.products[2]; 41 | const requestData = { 42 | accountData: accounts, 43 | currentAccount: accounts[0], 44 | }; 45 | 46 | window.fetch = jest.fn().mockResolvedValue({ 47 | json: async () => { 48 | return { 49 | found: 7, 50 | displaying: 7, 51 | moreAvailable: 'false', 52 | fromDate: '1722682119', 53 | toDate: '1725360519', 54 | sort: 'desc', 55 | transactions: [ 56 | { 57 | id: 412745936, 58 | amount: 100.0, 59 | accountId: 15522609, 60 | customerId: 2519572, 61 | status: 'active', 62 | description: 'Funds Transfer Loan repay', 63 | institutionTransactionId: '0000000000', 64 | postedDate: 1723982400, 65 | transactionDate: 1723982400, 66 | createdDate: 1725394407, 67 | }, 68 | ], 69 | }; 70 | }, 71 | status: 200, 72 | }); 73 | const result = await proccessSendRequest(product, requestData); 74 | expect(result.tableData.length).toEqual(1); 75 | }); 76 | 77 | test('should return tableData and response when identifier is account_owner_details', async () => { 78 | const product = data.products[5]; 79 | const requestData = { 80 | accountData: accounts, 81 | }; 82 | 83 | window.fetch = jest.fn().mockResolvedValue({ 84 | json: async () => { 85 | return { 86 | holders: [ 87 | { 88 | ownerName: 'FRED NURK', 89 | addresses: [ 90 | { 91 | ownerAddress: 92 | '72 CHRISTIE STREET ST. LEONARDS, NSW 2065', 93 | }, 94 | ], 95 | }, 96 | ], 97 | }; 98 | }, 99 | status: 200, 100 | }); 101 | const result = await proccessSendRequest(product, requestData); 102 | expect(result.tableData.length).toEqual(6); 103 | }); 104 | 105 | test('should return tableData and response when identifier is account_ach_details', async () => { 106 | const product = data.products[3]; 107 | const requestData = { 108 | accountData: accounts, 109 | }; 110 | 111 | window.fetch = jest.fn().mockResolvedValue({ 112 | json: async () => { 113 | return { 114 | found: 7, 115 | displaying: 7, 116 | moreAvailable: 'false', 117 | fromDate: '1722682119', 118 | toDate: '1725360519', 119 | sort: 'desc', 120 | transactions: [ 121 | { 122 | id: 412745936, 123 | amount: 100.0, 124 | accountId: 15522609, 125 | customerId: 2519572, 126 | status: 'active', 127 | description: 'Funds Transfer Loan repay', 128 | institutionTransactionId: '0000000000', 129 | postedDate: 1723982400, 130 | transactionDate: 1723982400, 131 | createdDate: 1725394407, 132 | }, 133 | ], 134 | }; 135 | }, 136 | status: 200, 137 | }); 138 | const result = await proccessSendRequest(product, requestData); 139 | expect(result.tableData.length).toEqual(1); 140 | }); 141 | 142 | test('should return tableData and response when identifier is available_balance', async () => { 143 | const product = data.products[4]; 144 | const requestData = { 145 | accountData: accounts, 146 | }; 147 | 148 | window.fetch = jest.fn().mockResolvedValue({ 149 | json: async () => { 150 | return { 151 | found: 7, 152 | displaying: 7, 153 | moreAvailable: 'false', 154 | fromDate: '1722682119', 155 | toDate: '1725360519', 156 | sort: 'desc', 157 | transactions: [ 158 | { 159 | id: 412745936, 160 | amount: 100.0, 161 | accountId: 15522609, 162 | customerId: 2519572, 163 | status: 'active', 164 | description: 'Funds Transfer Loan repay', 165 | institutionTransactionId: '0000000000', 166 | postedDate: 1723982400, 167 | transactionDate: 1723982400, 168 | createdDate: 1725394407, 169 | }, 170 | ], 171 | }; 172 | }, 173 | status: 200, 174 | }); 175 | const result = await proccessSendRequest(product, requestData); 176 | expect(result.tableData.length).toEqual(2); 177 | }); 178 | 179 | test('should checkForAccountError', async () => { 180 | const product = data.products[4]; 181 | const requestData = { 182 | accountData: [ 183 | { 184 | id: '15522609', 185 | }, 186 | ], 187 | }; 188 | try { 189 | await proccessSendRequest(product, requestData); 190 | } catch (err: any) { 191 | expect(err.cause).toEqual('warning'); 192 | } 193 | }); 194 | 195 | test('should check for Invalid product', async () => { 196 | const product = { 197 | name: 'new product', 198 | identifier: 'new_product', 199 | }; 200 | const requestData = { 201 | accountData: [ 202 | { 203 | id: '15522609', 204 | }, 205 | ], 206 | }; 207 | try { 208 | await proccessSendRequest(product, requestData); 209 | } catch (err: any) { 210 | expect(err.message).toEqual('Invalid product'); 211 | } 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/components/Product/helper.ts: -------------------------------------------------------------------------------- 1 | import { generateFetchHeaders, handleFetchCall } from '../../utils/helper'; 2 | 3 | /** 4 | * Process product API request 5 | * @param product product details 6 | * @param requestData application parameters 7 | * @returns product API response 8 | */ 9 | export const proccessSendRequest = async (product: any, requestData: any) => { 10 | const requestHeaders = await generateFetchHeaders( 11 | product.requestType, 12 | requestData 13 | ); 14 | switch (product.identifier.toLowerCase()) { 15 | case 'accounts': 16 | return getAccountsInformation(product, requestData, requestHeaders); 17 | case 'refresh_accounts': 18 | return refreshAccounts(product, requestData, requestHeaders); 19 | case 'transactions': 20 | return getTransactions(product, requestData, requestHeaders); 21 | case 'account_ach_details': 22 | return getAchDetails(product, requestData, requestHeaders); 23 | case 'available_balance': 24 | return getAvailableBalanceLive( 25 | product, 26 | requestData, 27 | requestHeaders 28 | ); 29 | case 'account_owner_details': 30 | return getAccountOwners(product, requestData, requestHeaders); 31 | default: 32 | throw new Error('Invalid product'); 33 | } 34 | }; 35 | 36 | /** 37 | * Get accounts information 38 | * @param product product details 39 | * @param requestData application parameters 40 | * @param requestHeaders requestHeaders 41 | * @returns get account information response 42 | */ 43 | const getAccountsInformation = async ( 44 | product: any, 45 | requestData: any, 46 | requestHeaders: any 47 | ): Promise => { 48 | const response = await handleFetchCall( 49 | product.api, 50 | requestData, 51 | requestHeaders 52 | ); 53 | const tableData = getTableData(response.accounts, product.columns); 54 | return { tableData, response }; 55 | }; 56 | 57 | /** 58 | * Refresh shared accounts 59 | * @param product product details 60 | * @param requestData application parameters 61 | * @param requestHeaders requestHeaders 62 | */ 63 | const refreshAccounts = async ( 64 | product: any, 65 | requestData: any, 66 | requestHeaders: any 67 | ): Promise => { 68 | return handleFetchCall(product.api, requestData, requestHeaders); 69 | }; 70 | 71 | /** 72 | * Get account transactions 73 | * @param product product details 74 | * @param requestData application parameters 75 | * @param requestHeaders requestHeaders 76 | * @returns get transaction response 77 | */ 78 | const getTransactions = async ( 79 | product: any, 80 | requestData: any, 81 | requestHeaders: any 82 | ): Promise => { 83 | const tableDataArray: any = []; 84 | const todayDate = new Date(); 85 | const endDate = Math.floor(todayDate.getTime() / 1000); 86 | todayDate.setMonth(todayDate.getMonth() - 1); 87 | const startDate = Math.floor(todayDate.getTime() / 1000); 88 | requestData.startDate = startDate; 89 | requestData.endDate = endDate; 90 | const response = await handleFetchCall( 91 | product.api, 92 | requestData, 93 | requestHeaders, 94 | requestData.currentAccount 95 | ); 96 | if (response.transactions.length > 0) { 97 | for (const trxn of response.transactions) { 98 | const date = new Date(trxn.postedDate * 1000).toLocaleDateString( 99 | 'en-us', 100 | { day: '2-digit', month: 'long', year: 'numeric' } 101 | ); 102 | tableDataArray.push({ 103 | date: date, 104 | description: trxn.description, 105 | amount: trxn.amount, 106 | currency: 'USD', 107 | }); 108 | } 109 | requestData.accountData.map((account: any) => { 110 | if (account.id === requestData.currentAccount.id) { 111 | account['transactions'] = tableDataArray; 112 | account['transactionsJson'] = response; 113 | } 114 | }); 115 | } 116 | 117 | return { tableData: tableDataArray, response: response }; 118 | }; 119 | 120 | /** 121 | * Get Account ACH details 122 | * @param product product details 123 | * @param requestData application parameters 124 | * @param requestHeaders requestHeaders 125 | * @returns Account ACH details response 126 | */ 127 | const getAchDetails = async ( 128 | product: any, 129 | requestData: any, 130 | requestHeaders: any 131 | ): Promise => { 132 | const accounts = requestData.accountData.filter((account: any) => 133 | ['checking', 'savings', 'moneyMarket'].includes(account.type) 134 | ); 135 | checkForAccountError(product, accounts); 136 | const tableDataArray = []; 137 | const jsonArray = []; 138 | const processRequest = []; 139 | for (const account of accounts) { 140 | processRequest.push( 141 | handleFetchCall(product.api, requestData, requestHeaders, account) 142 | ); 143 | } 144 | const responses: any = await Promise.all(processRequest); 145 | for (const [index, response] of responses.entries()) { 146 | const paymentInstruction = { 147 | id: accounts[index].id, 148 | accountNumber: response?.realAccountNumber, 149 | routingNumber: response?.routingNumber, 150 | }; 151 | tableDataArray.push(paymentInstruction); 152 | jsonArray.push(response); 153 | } 154 | 155 | checkForAccountError(product, jsonArray); 156 | return { tableData: tableDataArray, response: jsonArray }; 157 | }; 158 | 159 | /** 160 | * Get live available response 161 | * @param product product details 162 | * @param requestData application parameters 163 | * @param requestHeaders requestHeaders 164 | * @returns get available balance live response 165 | */ 166 | const getAvailableBalanceLive = async ( 167 | product: any, 168 | requestData: any, 169 | requestHeaders: any 170 | ): Promise => { 171 | const accounts = requestData.accountData.filter((account: any) => 172 | ['checking', 'savings', 'moneyMarket', 'cd'].includes(account.type) 173 | ); 174 | const tableDataArray = []; 175 | const jsonArray = []; 176 | const processRequest = []; 177 | for (const account of accounts) { 178 | processRequest.push( 179 | handleFetchCall(product.api, requestData, requestHeaders, account) 180 | ); 181 | } 182 | const responses = await Promise.all(processRequest); 183 | for (const response of responses) { 184 | const paymentInstruction = { 185 | id: response.id, 186 | accountNumber: response.realAccountNumberLast4, 187 | availableBalance: response.availableBalance, 188 | currency: response.currency, 189 | }; 190 | tableDataArray.push(paymentInstruction); 191 | jsonArray.push(response); 192 | } 193 | 194 | checkForAccountError(product, jsonArray); 195 | return { tableData: tableDataArray, response: jsonArray }; 196 | }; 197 | 198 | /** 199 | * Get account owners 200 | * @param product product details 201 | * @param requestData application parameters 202 | * @param requestHeaders requestHeaders 203 | * @returns account owners response 204 | */ 205 | const getAccountOwners = async ( 206 | product: any, 207 | requestData: any, 208 | requestHeaders: any 209 | ): Promise => { 210 | const accounts = requestData.accountData; 211 | const tableDataArray = []; 212 | const jsonArray = []; 213 | const processRequest = []; 214 | for (const account of accounts) { 215 | processRequest.push( 216 | handleFetchCall(product.api, requestData, requestHeaders, account) 217 | ); 218 | } 219 | const responses: any = await Promise.all(processRequest); 220 | for (const [index, response] of responses.entries()) { 221 | const holderDetails = response?.holders[0]; 222 | const paymentInstruction = { 223 | id: accounts[index].id, 224 | name: holderDetails.ownerName, 225 | address: holderDetails?.addresses[0].ownerAddress, 226 | }; 227 | tableDataArray.push(paymentInstruction); 228 | jsonArray.push(response); 229 | } 230 | 231 | return { tableData: tableDataArray, response: jsonArray }; 232 | }; 233 | 234 | /** 235 | * Process data to display on table 236 | * @param jsonData json response 237 | * @param columns table columns 238 | * @returns table response 239 | */ 240 | const getTableData = (jsonData: any, columns: any) => { 241 | return jsonData.map((data: any) => { 242 | const candidate: any = {}; 243 | columns.forEach((column: any) => { 244 | candidate[column.accessorKey] = data[column.accessorKey]; 245 | }); 246 | return candidate; 247 | }); 248 | }; 249 | 250 | const checkForAccountError = (product: any, jsonArray: any) => { 251 | if (jsonArray.length === 0) { 252 | const accountTypeError = new Error(); 253 | accountTypeError.message = product.error.accountError; 254 | accountTypeError.cause = 'warning'; 255 | throw accountTypeError; 256 | } 257 | }; 258 | -------------------------------------------------------------------------------- /src/tests/Mocks/accounts-data.ts: -------------------------------------------------------------------------------- 1 | const accounts = [ 2 | { 3 | id: '15415575', 4 | number: ' xxxx-xxxx-xxxx-0001', 5 | realAccountNumberLast4: '0001', 6 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0001', 7 | name: 'Transaction', 8 | displayName: 'Transaction', 9 | balance: 22327.3, 10 | type: ' checking', 11 | aggregationStatusCode: 0, 12 | status: 'active', 13 | customerId: '2488863', 14 | institutionId: '200003', 15 | balanceDate: '1722238773', 16 | aggregationSuccessDate: '1722238773', 17 | aggregationAttemptDate: '1722238773', 18 | createdDate: '1722238749', 19 | lastUpdatedDate: '1722274751', 20 | currency: 'AUD', 21 | lastTransactionDate: '1722238759', 22 | institutionLoginId: '2336229', 23 | detail: { 24 | availableBalanceAmount: '22327.3', 25 | openDate: '1677024000', 26 | }, 27 | financialinstitutionAccountStatus: 'OPEN', 28 | accountNickname: 'Transaction', 29 | marketSegment: 'personal', 30 | }, 31 | { 32 | id: '15415576', 33 | number: 'xxxx-xxxx-xxxx-0002', 34 | realAccountNumberLast4: '0002', 35 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0002', 36 | name: 'Savings', 37 | displayName: 'Savings', 38 | 39 | balance: '786000', 40 | type: 'savings', 41 | aggregationStatusCode: '0', 42 | status: 'active', 43 | customerId: '2488863', 44 | institutionId: '200003', 45 | balanceDate: '1722238773', 46 | aggregationSuccessDate: '1722238773', 47 | aggregationAttemptDate: '1722238773', 48 | createdDate: '1722238749', 49 | lastUpdatedDate: '1722274751', 50 | currency: 'USD', 51 | lastTransactionDate: '1722238763', 52 | institutionLoginId: '2336229', 53 | detail: { 54 | availableBalanceAmount: '786000', 55 | openDate: '1677024000', 56 | }, 57 | financialinstitutionAccountStatus: 'OPEN', 58 | accountNickname: 'savings', 59 | marketSegment: 'personal', 60 | transactions: [ 61 | { 62 | date: '19 July 2024', 63 | description: 'TransactionAndSaving debit 201', 64 | amount: -7.19, 65 | }, 66 | { 67 | date: '09 July 2024', 68 | description: 'TransactionAndSaving credit 191', 69 | amount: 7.09, 70 | }, 71 | ], 72 | transactionsJson: { 73 | found: 2, 74 | displaying: 2, 75 | moreAvailable: false, 76 | fromDate: 1720423617, 77 | toDate: 1723102017, 78 | sort: 'desc', 79 | transactions: [ 80 | { 81 | id: 409923846, 82 | amount: -7.19, 83 | accountId: 15415576, 84 | customerId: 2488863, 85 | status: 'active', 86 | description: 'TransactionAndSaving debit 201', 87 | memo: 'walmart', 88 | institutionTransactionId: '0000000000', 89 | postedDate: 1721390400, 90 | transactionDate: 1721390400, 91 | createdDate: 1723137871, 92 | }, 93 | { 94 | id: 409923852, 95 | amount: 7.09, 96 | accountId: 15415576, 97 | customerId: 2488863, 98 | status: 'active', 99 | description: 'TransactionAndSaving credit 191', 100 | memo: 'walmart', 101 | institutionTransactionId: '0000000000', 102 | postedDate: 1720526400, 103 | transactionDate: 1720526400, 104 | createdDate: 1723137871, 105 | }, 106 | ], 107 | }, 108 | }, 109 | { 110 | id: '15415577', 111 | number: 'xxxx-xxxx-xxxx-0003', 112 | realAccountNumberLast4: '0003', 113 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0003', 114 | name: 'Credit Card', 115 | displayName: 'Credit Card', 116 | 117 | balance: '-1952.71', 118 | type: 'creditCard', 119 | aggregationStatusCode: '0', 120 | status: 'active', 121 | customerId: '2488863', 122 | institutionId: '200003', 123 | balanceDate: '1722238773', 124 | aggregationSuccessDate: '1722238773', 125 | aggregationAttemptDate: '1722238773', 126 | createdDate: '1722238749', 127 | lastUpdatedDate: '1722274751', 128 | currency: 'AUD', 129 | lastTransactionDate: '1722238766', 130 | institutionLoginId: '2336229', 131 | detail: { 132 | creditAvailableAmount: '-1952.71', 133 | creditMaxAmount: '200000', 134 | paymentMinAmount: '100', 135 | paymentDueDate: '1658448000', 136 | }, 137 | financialinstitutionAccountStatus: 'OPEN', 138 | accountNickname: 'credit Card', 139 | marketSegment: 'personal', 140 | }, 141 | { 142 | id: '15415578', 143 | number: 'xxxx-xxxx-xxxx-0004', 144 | realAccountNumberLast4: '0004', 145 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0004', 146 | name: 'Loan', 147 | displayName: 'Loan', 148 | 149 | balance: '-600', 150 | type: 'loan', 151 | aggregationStatusCode: '0', 152 | status: 'active', 153 | customerId: '2488863', 154 | institutionId: '200003', 155 | balanceDate: '1722238773', 156 | aggregationSuccessDate: '1722238773', 157 | aggregationAttemptDate: '1722238773', 158 | createdDate: '1722238749', 159 | lastUpdatedDate: '1722274751', 160 | currency: 'AUD', 161 | lastTransactionDate: '1722238772', 162 | institutionLoginId: '2336229', 163 | detail: { 164 | interestRate: '6.8900', 165 | initialMlAmount: '40000', 166 | loanPaymentFreq: 'MNTH', 167 | paymentMinAmount: '25.05', 168 | availableBalanceAmount: '98573.29', 169 | loanPaymentType: 'PRINCIPAL AND_INTEREST', 170 | initialMlDate: '1583193600', 171 | nextPaymentDate: '1661126400', 172 | endDate: '1842134400', 173 | }, 174 | financialinstitutionAccountStatus: 'OPEN', 175 | accountNickname: 'Loan', 176 | marketSegment: 'personal', 177 | }, 178 | { 179 | id: '15415579', 180 | number: 'xxxx-xxxx-xxxx-0006', 181 | realAccountNumberLast4: '0006', 182 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0006', 183 | name: 'Mortgage', 184 | displayName: 'Mortgage', 185 | 186 | balance: '22327.3', 187 | type: 'mortgage', 188 | aggregationStatusCode: '0', 189 | status: 'active', 190 | customerId: '2488863', 191 | institutionId: '200003', 192 | balanceDate: '1722238773', 193 | aggregationSuccessDate: '1722238773', 194 | aggregationAttemptDate: '1722238773', 195 | createdDate: '1722238749', 196 | lastUpdatedDate: '1722274751', 197 | currency: 'AUD', 198 | lastTransactionDate: '1722238772', 199 | institutionLoginId: '2336229', 200 | detail: { 201 | interestRate: '0.0684', 202 | initialMlAmount: '800000', 203 | loanPaymentFreq: 'P1M', 204 | paymentMinAmount: '5237', 205 | availableBalanceAmount: '22327.3', 206 | loanPaymentType: 'PRINCIPAL_AND_INTEREST', 207 | initialMlDate: '1647302400', 208 | nextPaymentDate: '1718409600', 209 | endDate: '2594073600', 210 | }, 211 | financialinstitutionAccountStatus: 'OPEN', 212 | accountNickname: 'Mortgage', 213 | marketSegment: 'personal', 214 | }, 215 | { 216 | id: '15415580', 217 | number: 'xxxx-xxxx-xxxx-0005', 218 | realAccountNumberLast4: '0005', 219 | accountNumberDisplay: 'xxxx-xxxx-xxxx-0005', 220 | name: 'Interest Payment', 221 | displayName: 'Interest Payment', 222 | 223 | balance: '22327.3', 224 | type: 'cd', 225 | aggregationStatusCode: '0', 226 | status: 'active', 227 | customerId: '2488863', 228 | institutionId: '200003', 229 | balanceDate: '1722238773', 230 | aggregationSuccessDate: '1722238773', 231 | aggregationAttemptDate: '1722238773', 232 | createdDate: '1722238749', 233 | lastUpdatedDate: '1722274751', 234 | currency: 'AUD', 235 | lastTransactionDate: '1722238773', 236 | institutionLoginId: '2336229', 237 | detail: { 238 | availableBalanceAmount: '22327.3', 239 | maturityAmount: '10000', 240 | openDate: '1677024000', 241 | originationDate: '1677024000', 242 | maturityDate: '1708560000', 243 | }, 244 | financialinstitutionAccountStatus: 'OPEN', 245 | accountNickname: 'Interest Payment', 246 | marketSegment: 'personal', 247 | }, 248 | ]; 249 | 250 | export default accounts; 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Banking Reference Application 2 | 3 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application) 4 | 5 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_open-banking-reference-application&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application) 6 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_open-banking-reference-application&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application) 7 | [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_open-banking-reference-application&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application) 8 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_open-banking-reference-application&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application) 9 | 10 | ## Table of Contents 11 | 12 | - [Overview](#overview) 13 | - [References](#references) 14 | - [Set up](#set-up) 15 | - [Compatibility](#compatibility) 16 | - [Installation](#installation) 17 | - [Test](#test) 18 | - [Demo](#demo) 19 | 1. [Generate your credentials](#1-generate-your-credentials) 20 | 2. [Add credentials in the .env file](#2-add-credentials-in-the-env-file) 21 | 3. [Setup and run the application](#3-setup-and-run-the-application) 22 | 4. [Create your first customer](#4-create-your-first-customer) 23 | 5. [Add a bank account to customer](#5-add-a-bank-account-to-customer) 24 | 6. [Pull account information](#6-pull-account-information) 25 | 7. [Explore usecases](#7-usecases) 26 | - [Hosting Reference App](#hosting-reference-app) 27 | - [Contact Us](#contact-us) 28 | 29 | ## Overview 30 | 31 | The Open Banking Reference App allows you to explore [Mastercard's Open Banking Service (MOBS)](https://developer.mastercard.com/open-banking-us/documentation/) to incorporate it into your product. This application allows you to: 32 | 33 | - Create test customers 34 | - Retrieve the data from the shared accounts 35 | - Explore the solutions offered by Mastercard Open Banking 36 | 37 | > **IMPORTANT**: Please note that applications accessing the Mastercard Open Banking APIs must be hosted within US. 38 | 39 | ### References 40 | 41 | - [Test API ](https://developer.mastercard.com/open-banking-us/documentation/test-the-apis/) 42 | - [API Reference](https://developer.mastercard.com/open-banking-us/documentation/api-reference/) 43 | - [Connect Web SDK](https://developer.mastercard.com/open-banking-us/documentation/connect/connect-implementation/) 44 | 45 | ## Set up 46 | 47 | ### Compatibility 48 | 49 | - **Node (v14+)** 50 | - **ReactJS (v18.3.1)** 51 | 52 | This application is built using the React framework. React requires Node version 14+. 53 | However, It is recommended that you use one of NodeJS's LTS releases or one of the [more general recent releases](https://github.com/nodejs/Release). A Node version manager such as [nvm](https://github.com/creationix/nvm) (Mac and Linux) or [nvm-windows](https://github.com/coreybutler/nvm-windows) can help with this. 54 | 55 | ### Installation 56 | 57 | Before using the Reference App, you will need to set up a project in the local machine. 58 | The following commands will help you to get the latest code: 59 | 60 | ```shell 61 | git clone https://github.com/Mastercard/open-banking-reference-application.git 62 | 63 | cd open-banking-reference-application 64 | ``` 65 | 66 | ### Test 67 | 68 | You can run the following command to execute the test cases against the latest version of the Reference App: 69 | 70 | ```shell 71 | npm run test 72 | ``` 73 | 74 | ![test case result page](docs/test_case_result.png) 75 | 76 | ## Demo 77 | 78 | ### 1. Generate your credentials 79 | 80 | - Login to the [Mastercard developer's portal](https://developer.mastercard.com/product/open-banking/) 81 | - Log in and click the **Create New Project** button at the top left of the page. 82 | - Enter your project name and select Open Banking as the API service, then click on the **Proceed** button. 83 | - Select **United States of America** in the Commercial Countries drop down list, and click on the **Proceed** button. 84 | - Enter a description of your project on the next page, and click on the **Create Project** button. 85 | - Take note of your Partner ID, Partner Secret and App Key. These will be required in the following sections. 86 | 87 | For more details see [Onboarding](https://developer.mastercard.com/open-banking-us/documentation/onboarding/). 88 | 89 | ![Alt Text](https://user-images.githubusercontent.com/3964455/221236073-5661d083-0a04-4d46-9710-3c0c8c9e9a6a.gif) 90 | 91 | ### 2. Add credentials in the .env file 92 | 93 | The Open Banking Reference App needs Sandbox API credentials adding to the `.env` file to make the API calls: 94 | 95 | 1. Create the `.env` file. 96 | ```shell 97 | cp .env.template .env 98 | ``` 99 | 2. Update the `.env` file with your Sandbox API credentials generated in step 1. 100 | 3. The default value of `REACT_APP_AUTO_CREATE_CUSTOMER` is set to `false`. If the customer creation needs to be initated automatically then the value should be set to `true` 101 | 102 | ### 3. Setup and run the application 103 | 104 | - ##### Run without docker 105 | 106 | The following command will install the required depdendancies on your machine. (This command should be executed during the initial setup) 107 | 108 | ``` 109 | npm i 110 | ``` 111 | 112 | Execute the following command to start the Reference App: 113 | 114 | ```shell 115 | npm start 116 | ``` 117 | 118 | - ##### Run with docker 119 | 120 | **Pre-requisites** - Docker installed and running on your machine: https://docs.docker.com/get-docker/ 121 | 122 | The following command will create the docker image of the application and will start the application. 123 | 124 | ``` 125 | docker compose up 126 | ``` 127 | 128 | Launch the web browser and navigate to http://localhost:4000 to view the application. 129 | 130 | **Note:** To update the docker image for the reference application, execute the command `docker compose build`, followed by `docker compose up` to run application. 131 | 132 | When the application is launched in a browser, it prompts either to proceed with demo or go to GitHub. Select **View Demo**. 133 | This will redirect you to the first step of the user flow. 134 | 135 | ![landing page](docs/landing-page.png) 136 | 137 | ### 4. Create your first customer 138 | 139 | - To access any financial data, first you need to create a customer. 140 | - This can be done either manually or automatically, depending on `REACT_APP_AUTO_CREATE_CUSTOMER` flag value in the `.env` file. 141 | If `REACT_APP_AUTO_CREATE_CUSTOMER` is set to `false`, application will prompt you to provide a unique identifier for the customer. To proceed further, select **Next**. 142 | 143 | ![create customer page](docs/create-customer.png) 144 | 145 | If the `REACT_APP_AUTO_CREATE_CUSTOMER` is set to `true` then the customer will be created automatically. 146 | 147 | ![create customer automatic page](docs/create-customer-auto.png) 148 | 149 | ### 5. Add a bank account to customer 150 | 151 | Now that you have a **Customer ID**, the next step is to add a bank account. The screen lists a name of the Financial Institution and credentials to use during Connect flow. 152 | To start, select **Connect Bank Account**: 153 | 154 | ![add bank account page](docs/add-bank-account.png) 155 | 156 | This flow is a simulation of what a customer will see when they share their financial data. 157 | In Connect flow: 158 | 159 | 1. Search for **FinBank Profiles - A**. 160 | 2. Click **Next**. 161 | 3. Type test and profile_02 when asked for a username and password. 162 | 4. Select all accounts, and then click **Save**. 163 | 5. Click **Submit**. 164 | 165 |

166 | 167 |

168 | 169 | ### 6. Pull account information 170 | 171 | At this point having customer ID allows you to retrieve the financial data. The Reference App shows examples of how to retrieve following data elements with the help of Mastercard open banking API's: 172 | 173 | 1. Account ID 174 | 2. Account name 175 | 3. Account type 176 | 4. Balance 177 | 5. Currency 178 | 179 | ![account information page](docs/account-information.png) 180 | 181 | ### 7. Usecases 182 | 183 | The use cases section provides you with an overview of the different solutions offered by Mastercard Open Banking. 184 | 185 | - **Lend** 186 | - Investigate the ways of generating and obtaining the lending reports, including Verification of Assets, Verification of Income and Cash Flow Report etc. MOBS solution allows you to obtain these reports in 187 | both PDF and JSON format. 188 | 189 | ![lend](docs/lend.png) 190 | 191 | - **Manage** 192 | - Lean how to request the transaction details for a particular account and data it contains. 193 | 194 | ![manage](docs/manage.png) 195 | 196 | - **Pay** 197 | - Discover how to obtain the key elements of the customers account required to initiate a payment, such as ACH details and available balance. 198 | 199 | ![pay](docs/pay_money_transfer_details.png) 200 | 201 | ![pay2](docs/pay_available_balance.png) 202 | 203 | ![pay3](docs/pay_account_owner_details.png) 204 | 205 | ## Hosting Reference App 206 | 207 | To host the Reference App on your server, run the following command to create the application build: 208 | 209 | ``` 210 | npm run build 211 | ``` 212 | 213 | Refer to the below code snippet for creating an [express](https://www.npmjs.com/package/express) application. Note, that to handling proxy requests to MOBS APIs, we are using [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). Alternatively, you can set up your proxy server for managing CORS (cross-origin-resource-sharing). 214 | 215 | ``` 216 | const express = require('express'); 217 | const {createProxyMiddleware} = require('http-proxy-middleware'); 218 | const app = express(); 219 | const port = process.env.PORT || 4000; 220 | 221 | app.use(express.static('build')); 222 | app.use( 223 | ['/aggregation', '/connect', '/decisioning'], 224 | createProxyMiddleware({ 225 | target: 'https://api.finicity.com/', 226 | changeOrigin: true, 227 | }) 228 | ); 229 | 230 | app.listen(port, () => { 231 | console.log(`Example app listening on port ${port}`); 232 | }); 233 | ``` 234 | 235 | ## Contact Us 236 | 237 | Have issues or concerns regarding the application? 238 | Please create an issue in the GitHub and our team will try to address the issue as soon as possible. 239 | -------------------------------------------------------------------------------- /src/components/Product/Product.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState, useEffect } from 'react'; 2 | import { 3 | Grid, 4 | Stack, 5 | Chip, 6 | Typography, 7 | Button, 8 | ToggleButtonGroup, 9 | ToggleButton, 10 | Select, 11 | MenuItem, 12 | SelectChangeEvent, 13 | Divider, 14 | CircularProgress, 15 | Tooltip, 16 | ButtonGroup, 17 | } from '@mui/material'; 18 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 19 | import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; 20 | import { useDispatch, useSelector } from 'react-redux'; 21 | 22 | import { 23 | CurlCommand, 24 | DataTable, 25 | ExternalIcon, 26 | JsonViewer, 27 | SnackBarNotification, 28 | } from '../../components'; 29 | import { snackbarActions } from '../../store/slices/snackbar'; 30 | import { accountsRefreshedActions } from '../../store/slices/accounts-refreshed'; 31 | 32 | import './Product.css'; 33 | import data from './data'; 34 | import { proccessSendRequest } from './helper'; 35 | 36 | export default function Product({ product, requestData, body }: any) { 37 | const dispatch = useDispatch(); 38 | const accountsRefreshed = useSelector( 39 | (state: any) => state.accountsRefreshed.refreshed 40 | ); 41 | const [currentProduct, setCurrentProduct] = useState( 42 | data.products.find((item) => item.identifier.toLowerCase() === product) 43 | ); 44 | useEffect(() => { 45 | setCurrentProduct( 46 | data.products.find( 47 | (item) => item.identifier.toLowerCase() === product 48 | ) 49 | ); 50 | setShowRequest(false); 51 | }, [product]); 52 | const [tableData, setTableData] = useState(); 53 | const [jsonData, setJsonData] = useState(); 54 | const [loading, setLoading] = useState(false); 55 | const [showResult, setShowResult] = useState(false); 56 | const [disableRequest, setDisableRequest] = useState(false); 57 | const [displayDataType, setDisplayDataType] = useState('table'); 58 | const [accountDisplay, setAccountDisplay] = useState( 59 | requestData.accountDisplayNames[0] 60 | ); 61 | const [currentAccount, setCurrentAccount] = useState( 62 | requestData.accountData[0] 63 | ); 64 | const [showRequest, setShowRequest] = useState(false); 65 | 66 | /** 67 | * Report change event handler 68 | * @param event SelectChangeEvent 69 | */ 70 | const handleAccountChangeSelect = (event: SelectChangeEvent) => { 71 | setAccountDisplay( 72 | requestData.accountDisplayNames.find( 73 | (account: any) => event.target.value === account 74 | ) 75 | ); 76 | setCurrentAccount( 77 | requestData.accountData.find( 78 | (account: any) => event.target.value === account.displayName 79 | ) 80 | ); 81 | requestData.accountData.map((account: any) => { 82 | if (account.displayName === event.target.value) { 83 | if (account.transactions) { 84 | setTableData(account.transactions); 85 | setJsonData(account.transactionsJson); 86 | setShowResult(true); 87 | } else { 88 | setShowResult(false); 89 | } 90 | } 91 | }); 92 | requestData.currentAccount = requestData.accountData.find( 93 | (account: any) => event.target.value === account.displayName 94 | ); 95 | setShowRequest(false); 96 | }; 97 | 98 | /** 99 | * Show request event handler 100 | * @param event event 101 | */ 102 | const handleShowRequest = (event: any) => { 103 | event.preventDefault(); 104 | setShowRequest(!showRequest); 105 | }; 106 | 107 | /** 108 | * Send request handler 109 | */ 110 | const handleSendRequest = async () => { 111 | setLoading(true); 112 | setDisableRequest(true); 113 | requestData.currentAccount = currentAccount; 114 | try { 115 | if (currentProduct.identifier === 'refresh_accounts') { 116 | dispatch( 117 | snackbarActions.open({ 118 | message: 'Refreshing shared accounts', 119 | severity: 'info', 120 | }) 121 | ); 122 | } 123 | const { tableData, response } = await proccessSendRequest( 124 | currentProduct, 125 | requestData 126 | ); 127 | setTableData(tableData); 128 | setJsonData(response); 129 | setShowRequest(false); 130 | if (currentProduct.identifier != 'refresh_accounts') { 131 | setShowResult(true); 132 | } else { 133 | dispatch( 134 | snackbarActions.open({ 135 | message: 'Accounts refreshed successfully', 136 | severity: 'success', 137 | timeout: 3000, 138 | }) 139 | ); 140 | dispatch(accountsRefreshedActions.refreshed()); 141 | } 142 | setDisplayDataType('table'); 143 | } catch (error: any) { 144 | if (error.message) { 145 | dispatch( 146 | snackbarActions.open({ 147 | message: error.message, 148 | severity: error.cause ? 'warning' : 'error', 149 | }) 150 | ); 151 | } 152 | } 153 | setLoading(false); 154 | setDisableRequest(false); 155 | }; 156 | 157 | /** 158 | * Change response data type handler 159 | * @param event MouseEvent 160 | * @param dataType response data type 161 | */ 162 | const handleDisplayDataTypeChange = ( 163 | event: React.MouseEvent, 164 | dataType: string 165 | ) => { 166 | setDisplayDataType(dataType || displayDataType); 167 | }; 168 | 169 | return ( 170 | 171 | 172 | 173 | 179 | 185 | {currentProduct.name} 186 | 187 | 192 | 193 | 194 | {currentProduct.type != 'lend' && ( 195 | 203 | 211 | 233 | 234 | 235 | )} 236 | 237 | 238 | 239 | {currentProduct.type != 'lend' && ( 240 | 246 | 247 | 248 | {currentProduct.description} 249 | 250 | 251 | 252 | )} 253 | {currentProduct.type === 'lend' && ( 254 | 255 |
256 | {currentProduct.description} 257 |
258 | )} 259 |
260 | {['transactions'].includes(product.toLowerCase()) && ( 261 | 262 | 278 | 279 | )} 280 | 281 | 304 | 305 | {showRequest && ( 306 | 307 | 312 | 313 | )} 314 | 315 | {['money_transfer_details', 'available_balance'].includes( 316 | product.toLowerCase() 317 | ) && 318 | !showResult && ( 319 | 320 | 321 | 322 | )} 323 | 324 | {showResult && ( 325 | 326 | 332 | 339 | 343 | Table 344 | 345 | 346 | JSON 347 | 348 | 349 | 350 | {displayDataType === 'json' && ( 351 |
352 | 353 |
354 | )} 355 | {displayDataType === 'table' && ( 356 | 361 | )} 362 |
363 | )} 364 |
365 | 366 | 367 |
368 | ); 369 | } 370 | --------------------------------------------------------------------------------