├── 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 | You need to enable JavaScript to run this app.
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 |
21 |
22 |
23 |
29 |
34 | developers
35 |
36 |
41 |
42 |
50 |
51 |
52 |
57 | REFERENCE APP
58 |
59 | Open Banking
60 |
61 |
67 | }
70 | variant='outlined'
71 | sx={{
72 | borderRadius: '25px',
73 | borderWidth: '2px',
74 | padding: '5px 25px',
75 | borderColor: '#FFFFFF',
76 | color: 'white',
77 | }}
78 | >
79 | View on Github
80 |
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 |
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 | {' '}
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 |
72 |
73 |
74 |
75 |
76 |
82 | View demo
83 |
84 |
89 |
95 | }
96 | variant='outlined'
97 | className='view-on-github__button flex items-center'
98 | >
99 | View on Github
100 |
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 |
113 | {data.reports.map((report: any) => (
114 |
118 | {report?.name}
119 |
120 | ))}
121 |
122 |
123 |
124 |
125 |
130 |
131 |
132 | {!currentReport.json && (
133 |
140 | Generate report
141 | {disableGenerateReport && (
142 |
149 | )}
150 |
151 | )}
152 | {currentReport.json && (
153 |
159 | Download JSON report
160 |
161 | )}
162 | {currentReport.pdf && (
163 |
169 | Download PDF report
170 |
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 | [](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application)
4 |
5 | [](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application)
6 | [](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application)
7 | [](https://sonarcloud.io/summary/new_code?id=Mastercard_open-banking-reference-application)
8 | [](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 | 
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 | 
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 | 
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 | 
144 |
145 | If the `REACT_APP_AUTO_CREATE_CUSTOMER` is set to `true` then the customer will be created automatically.
146 |
147 | 
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 | 
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 | 
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 | 
190 |
191 | - **Manage**
192 | - Lean how to request the transaction details for a particular account and data it contains.
193 |
194 | 
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 | 
200 |
201 | 
202 |
203 | 
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 |
222 | Send request
223 | {loading && (
224 |
231 | )}
232 |
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 |
270 | {requestData.accountDisplayNames.map(
271 | (displayName: any) => (
272 |
273 | {displayName}
274 |
275 | )
276 | )}
277 |
278 |
279 | )}
280 |
281 |
285 | {!showRequest && (
286 |
287 |
288 |
289 | )}
290 | {showRequest && (
291 |
292 |
293 |
294 | )}
295 |
301 | cURL command
302 |
303 |
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 |
--------------------------------------------------------------------------------