diff --git a/.env.example b/.env.example index 8672917..ea7fec1 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,20 @@ -DOCK_API_URL=https://api-testnet.dock.io -DOCK_API_TOKEN= # you can generate a key at https://certs.dock.io/keys +NEXT_PUBLIC_DOCK_API_URL="https://api-testnet.dock.io" + +DOCK_API_TOKEN= ##DOCK_API_TOKEN + DOCK_API_DID= # the DID to use for the issuer. You can generate one here: https://certs.dock.io/dids -NEXT_PUBLIC_SERVER_URL= # the URL where this app is listening (e.g. http://192.168.0.100:3000 or http://localhost:3000) +NEXT_PUBLIC_SERVER_URL= http://localhost:3000 + +# Proof-Request Template Ids +NEXT_PUBLIC_QUOTIENT_LOAN_PROOF_TEMPLATE_ID= ## +NEXT_PUBLIC_FORSUR_ENROLLMENT_PROOF_TEMPLATE_ID= ## +NEXT_PUBLIC_FORSUR_VERIFICATION_PROOF_TEMPLATE_ID= ## +NEXT_PUBLIC_URBANSCAPE_BANKBIO_TEMPLATE_ID= ## +NEXT_PUBLIC_URBANSCAPE_CREDITSCORE_TEMPLATE_ID= ## +NEXT_PUBLIC_BANK_IDENDITY_TEMPLATE_ID= ## + +# Organization Profiles +NEXT_PUBLIC_QUOTIENT_ISSUER_ID= ## +NEXT_PUBLIC_FORSUR_ISSUER_ID= ## +NEXT_PUBLIC_EQUINET_ISSUER_ID= ## + diff --git a/.eslintrc.json b/.eslintrc.json index 71b99b9..e1eb3a5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,9 +7,21 @@ "browser": true, "es6": true }, - "extends": ["next", "eslint:recommended", "plugin:react/recommended", "airbnb-base"], + "extends": [ + "next", + "eslint:recommended", + "plugin:react/recommended", + "airbnb-base" + ], "rules": { - "no-use-before-define": ["error", { "functions": false, "classes": true, "variables": true }], + "no-use-before-define": [ + "error", + { + "functions": false, + "classes": true, + "variables": false + } + ], "class-methods-use-this": "off", "no-console": "off", "no-nested-ternary": "off", @@ -27,16 +39,49 @@ "indent": "off", "implicit-arrow-linebreak": "off", "function-paren-newline": "off", - "no-confusing-arrow": "off" + "no-confusing-arrow": "off", + "no-unused-vars": "off", + "no-trailing-spaces": "off", + "import/prefer-default-export": "off", + "no-shadow": "off", + "react/react-in-jsx-scope": "off", // Disable react/react-in-jsx-scope rule + "linebreak-style": ["off"], + "no-plusplus":"off" }, "settings": { "import/resolver": { + "node": { + "extensions": [ + ".js", + ".jsx", + "ts", + "tsx" + ], + "moduleDirectory": [ + ".", + "node_modules" + ] + }, "alias": { "map": [ - ["components", "./components"], - ["utils", "./utils"] + [ + "components", + "./components" + ], + [ + "utils", + "./utils" + ], + [ + "store", + "./store" + ], + [ + "lib", + "./lib" + ] ] } } } -} +} \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index ab9dd08..9e93aed 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,178 @@ -# Dock Bank Demo App +# Dock Sales Demo -This is a demo application showing some real use cases of [Dock API](https://docs.api.dock.io) for credential issuance and verification. +## Overview -## Getting Started +The Dock Sales Demo showcases the integration of KYC, biometric services, and verifiable credentials to demonstrate the capabilities of Dock's API and wallet. It simulates a customer journey through KYC verification, credential issuance, and verification across various scenarios, emphasizing integration ease and user experience. -First, setup a .env file in the project's root folder +### Workflow Steps -```bash -DOCK_API_URL=https://api-testnet.dock.io -DOCK_API_TOKEN= # you can generate a key at https://certs.dock.io/keys -DOCK_API_DID= # the DID to use for the issuer. You can generate one here: https://certs.dock.io/dids -NEXT_PUBLIC_SERVER_URL= # the URL where this app is listening (e.g. http://192.168.0.100:3000 or http://localhost:3000) -``` +1. 🚀 User initiates the KYC process. +2. 🔒 Biometric verification is conducted by scanning a QR code. +3. ✅ Upon successful verification, the user is issued verifiable credentials. +4. 🔍 The user presents the credentials for verification. -Then, run the development server: +*Note: The Dock wallet app, with the biometric service plugin enabled, is required.* -```bash -npm run dev -- -H IP_OR_HOSTNAME_WHERE_APP_IS_RUNNING -# or -yarn dev -- -H IP_OR_HOSTNAME_WHERE_APP_IS_RUNNING -``` +## Technologies Used -The `-- -H [IP]` option above is needed to allow scanning of the credentials and presentations to work in your wallet app. +- Next.js 12.2.3 +- Dock API for decentralized identity and credential management. +- Integration with biometric verification services on the mobile app. -Open [NEXT_PUBLIC_SERVER_URL] with your browser to see the app. +## 📚 Libraries and Dependencies -You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. +Our project, "dock-demo," incorporates a variety of technologies for optimal performance, state management, and UI aesthetics: -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. +- **Next.js:** The core framework provides server-side rendering and static site generation. +- **React:** Enables building our UI with a component-based approach. +- **Tailwind CSS:** Allows for efficient styling within JSX, promoting rapid UI development. +- **Axios:** Facilitates promise-based HTTP requests for external API communication. +- **React Hook Form:** Optimizes form handling with minimal re-renders. +- **Lucide-react:** Enhances UI design with customizable vector icons. +- **Zustand:** Simplifies global state management with a minimalistic store. +- **Date-fns:** Offers comprehensive JavaScript date manipulation tools. +- **Radix UI & TanStack React Table:** Provides low-level UI primitives and a lightweight table component for creating accessible, custom designs. +- **PostCSS & Autoprefixer:** Transforms CSS with JS for future-proofing and cross-browser compatibility. +- **ESLint:** Maintains code quality and consistency. +- **Shadcn-ui/ui:** A UI component library from [shadcn-ui/ui on GitHub](https://github.com/shadcn-ui/ui), enhancing our application with a set of pre-designed, customizable UI components for a consistent and modern interface. + +Each library and dependency has been carefully selected for its performance, reliability, and the support it offers to our development process. + +## 📁 Folder Structure + +- `/pages/api` - Server side API integration and communication with Dock. +- `/pages/org` - Organization pages for testing functionality. +- `/components/ui` - Interface components from ShadCn library. +- `/_credentials` - Credential Schemas used for issuing credentials. +- `/hooks` - Handle QR generation and verification and revoke credentials. +- `/store` - State management for user information and Qr code Status. +- `/utils` - Utility functions for API and server side communication for credentials and qr code. + +Note: Most of the functions are documented inside of each file. + +## Main Functions & Methods + +### State management + +The Qr code state is handle by `/store/qrCodeStore.js` folder, using the Zustand library, for updating the proof template Id and verification state. +Qr code track this states: + + proofTemplateId -> Dock Cert Verification template id + + qrCodeUrl -> Generated url from generateQR() function + + proofID -> Unique Id for each Qr code generated + + isLoading -> Boolean for loading code generation + + verified -> Is verified by user, default to false + + verificationError -> Boolean state for error handling + + retrievedData -> Credential data retrieved when is verified + +Each value also contain setter function. + +### Main Functions + +Under folder `/utils` are the files that handle all the communication with Dock API service. + +`createRegistry(policyDid, type)` -> Creates Registry for a new credential. + +`createCredential(registryId, credential)` -> Create new credential from registryId & credential schema. + +`distributeCredential(credential)` -> Send a signed credential to a Did or email. + +`issueRevokableCredential = async (credential, setRevokableCredential)` -> Entire process that carrie all previews functions. +revokeCredential = async (registryId, credentialId) -> Revoke credit score credential with a given registry and credential ids. + + +### Methods + +There is 3 important methods under `/hooks` folder wich make the application workflow: + +`useQrCode = ({ proofTemplateId })` -> +The useQrCode hook orchestrates the generation of QR codes associated with proof requests. It leverages state management through qrCodeStore, handling loading states, verification flags, and potential errors. Upon calling handleGenerateQR, it sets up the necessary parameters, initiates QR code creation via handleCreateQr, and updates relevant states accordingly. + +`useVerifyProof = ()` -> +This React hook, useVerifyProof, facilitates verifying proof requests tied to a QR code. It utilizes intervals to periodically check the verification status. Upon successful verification, it updates relevant states, such as user DID, verification status, and retrieved data. In case of errors, it handles retries and sets a verification error flag. + +### Main Component + +`QrCodeAuthentication` + +Hooks methods are use in single component `components/qrcode/qr-auth.jsx` with two responsabilities, generate and verify a Qr code using the previews hooks `useQrCode` & `useVerifyProof`. + +The `QrCodeAuthentication` component manages QR code authentication flow. It renders UI elements conditionally based on whether the QR code is verified or not. If not verified, it displays QR code verification UI provided by VerifyQrCode component along with optional descriptive texts before and after. Upon verification, it shows a refresh icon to allow users to retry the authentication process. Additionally, it lists required credentials using CredentialCards component. + +## 🎮 Running the Project + +### 📓Prerequisites + +- Node.js and npm installed. +- Dock account with API access. + +### 🔑 Configuration + +To configure the project, set up environment variables. Copy the `.env.example` file to a new file named `.env` and fill in the variables: + +- `NEXT_PUBLIC_DOCK_API_URL`: URL to the Dock API, set to the testnet endpoint. +- `DOCK_API_TOKEN`: Your API token for authenticating with the Dock API. +- `DOCK_API_DID`: The DID to use for the issuer. You can generate a DID [here](https://certs.dock.io/dids). +- `NEXT_PUBLIC_SERVER_URL`: The URL of your server, defaulting to `http://localhost:3000` for local development. + +### Proof-Request Template IDs and Organization Profiles + +Create these IDs from [Certs Dock](https://certs.dock.io/). Fill in the template IDs and issuer IDs for your proof requests and organization profiles. These are crucial for the functioning of your verifiable credentials within the demo. + +Ensure all these configurations are set properly in your `.env` file before running the project to ensure smooth operation and connectivity to the necessary services. + +### 🛠️ Installation and Deployment + +1. Clone the repo: `git clone ` +2. Install dependencies: `npm install` or `yarn` +3. Start the project: `npm start` or `yarn dev` + +## 🧪 Testing the Project Workflow + +Before starting, remember to get the Dock Wallet mobile app, with the biometric service plugin enabled, is required. The Dock Wallet app is available on PlayStore or AppStore. + +The demo workflow consists of going through the KYC process filling the formularies and scanning each QR code to get authenticated and test the organizations ecosystems. + +It's based on 3 ecosystems: + +**Quotient:** Get BankId and Credit Score credentials and Apply for an auto loan. + +**Equinet:** Revoke Credit Score credential and issue a new one. + +**Urbanscape:** Apply for an apartment providing your BankId and Credit Score credentials. + +**1. Create a Bank account:** On the main page organizations click on “New Bank Account” from Quotient. + +Fill the formulary required data and click on “Submit Application”. This will take you to the first QR code we need to scan with the Dock Wallet app using the option “Scan”. This QR code will trigger the biometric verification process on the mobile to get the Biometric credential. + +Once QR code verification is successful, you will receive 2 credentials in your Dock Wallet, “Credit Score credential” and “BankId credential”. + +**2. Obtain Auto Loan:** On the main page click on “Obtain Auto Loan” from Quotient. + +Identify yourself scanning the QR code using your Dock Wallet app and select the required credentials: Biometric, BankId, and Credit Score. + +On verification success will automatically fill the fields “First Name, Last Name, Street Address”, from your BankId credential details, then click on “Submit Application” to continue. + +On the success page, you will find a second QR code to scan with the Dock Wallet to proof your Credit Score from your credential. + +**3. Revoke & Reissue Credit Score:** On the main page click on “Visit Site” from Equinet. This page is the representation of a Credit Score credential history. You can revoke the actual Credit Score credential and issue another with one action. Click on “Revoke and Issue New credential” to test the functionality. Note: you need to complete previous steps to own this credential and revoke it. + +**4. Apply for an apartment:** On the main page click on “Visit Site” from Urbanscape. Identify yourself scanning the QR code using your Dock Wallet app and select the required credentials: Biometric, BankId, and Credit Score. + +On verification success will automatically fill the fields “First Name, Last Name, Street Address”, from your BankId credential details, then click on “Submit Application” to continue. + +On the success page, you will find a second QR code to scan and proof your Credit Score from your credential. + +With this, we finish the workflow from the application where we can have an overview of some usage of the decentralized identity and credentials management. + +## Resources + +- [Dock](https://www.dock.io/) +- [Dock API Documentation](https://docs.api.dock.io/) +- [Next.js](https://nextjs.org/) diff --git a/_credentials/equinet.js b/_credentials/equinet.js new file mode 100644 index 0000000..cab64c3 --- /dev/null +++ b/_credentials/equinet.js @@ -0,0 +1,40 @@ +import { v4 as uuidv4 } from "uuid"; +import { dockUrl } from "utils/constants"; +import { validateEmail } from "utils/validation"; + +export function createCreditScoreCredential({ receiverDid, recipientEmail, creditScore }) { + + console.log("Creating EquiNet - Credit Score Credential for:", receiverDid); + + const credentialPayload = { + url: `${dockUrl}/credentials`, + body: { + anchor: false, + distribute: true, + credential: { + name: "EquiNet - Credit Score", + description: "This schema represents a Verified Credit Score Credential, issued by EquiNet. It standardizes the presentation of credit scores for reliable and efficient verification processes.", + type: [ + "VerifiableCredential", + "EquiNetCreditScore" + ], + issuer: { + name: "EquiNET", + description: "EquiNet is the credit bureau.", + logo: "https://img.dock.io/9f327cafda3be5f0cff0da2df44c55da", + id: process.env.NEXT_PUBLIC_EQUINET_ISSUER_ID + }, + subject: { + id: receiverDid, + credit_score: creditScore, + } + } + } + } + + if (recipientEmail && recipientEmail.length > 2 && validateEmail(recipientEmail)) { + credentialPayload.recipientEmail = recipientEmail + } + + return credentialPayload; +} diff --git a/_credentials/forsur.js b/_credentials/forsur.js new file mode 100644 index 0000000..bc36698 --- /dev/null +++ b/_credentials/forsur.js @@ -0,0 +1,50 @@ +import { v4 as uuidv4 } from "uuid"; +import { dockUrl } from "utils/constants"; + +export function createBiometricsCredential({ + receiverDid, + recipientEmail +} +) { + + console.log("Creating ForSur - Biometric Enrollment Credential for:", receiverDid); + + const credentialPayload = { + url: `${dockUrl}/credentials`, + body: { + anchor: false, + algorithm: "dockbbs+", + distribute: true, + credential: { + id: `https://creds-testnet.dock.io/${uuidv4()}`, + name: "ForSur - Biometric", + description: "The \"ForSur - Biometric\" schema is specifically developed for the secure registration and storage of biometric data.", + type: [ + "VerifiableCredential", + "ForSurBiometric" + ], + issuer: { + name: "Forsur", + description: "Forsur is the biometric provider.", + logo: "https://img.dock.io/80f154126a78bba321b413c3ffb8d4a7", + id: process.env.NEXT_PUBLIC_FORSUR_ISSUER_ID + }, + subject: { + id: receiverDid, + biometric: { + id: uuidv4(), + created: new Date().toISOString(), + data: "some biometric data", + } + } + } + } + }; + + if (recipientEmail && recipientEmail.length > 2 && validateEmail(recipientEmail)) { + credentialPayload.recipientEmail = recipientEmail + } + + return credentialPayload + +} diff --git a/_credentials/quotient.js b/_credentials/quotient.js new file mode 100644 index 0000000..327ba55 --- /dev/null +++ b/_credentials/quotient.js @@ -0,0 +1,52 @@ +import { v4 as uuidv4 } from 'uuid'; +import { dockUrl } from 'utils/constants'; + +export function createBankIdCredential({ + receiverDid, + recipientEmail, + receiverName, + receiverAddress, + biometricData +}) { + console.log('Creating Quotient Bank Identity Credential for:', receiverDid); + + const credentialPayload = { + url: `${dockUrl}/credentials`, + body: { + anchor: false, + algorithm: 'dockbbs+', + distribute: true, + credential: { + id: `https://creds-testnet.dock.io/${uuidv4()}`, + name: 'Quotient - Bank Identity', + description: "The \"Quotient - Bank Identity\" schema provides a secure and standardized format for representing key aspects of an individual's bank identity.", + type: [ + 'VerifiableCredential', + 'QuotientBankIdentity' + ], + issuer: { + name: 'Quotient Credit Union', + description: 'Quotient is our credit union', + logo: 'https://img.dock.io/06d78272268c606a172d5fd1cd559b46', + id: process.env.NEXT_PUBLIC_QUOTIENT_ISSUER_ID + }, + subject: { + id: receiverDid, + name: receiverName, + address: receiverAddress.address, + city: receiverAddress.city, + zip: receiverAddress.zip, + state: receiverAddress.state, + account_number: `ABC${uuidv4()}`, + biometric: biometricData + } + } + } + }; + + if (recipientEmail && recipientEmail.length > 2 && validateEmail(recipientEmail)) { + credentialPayload.recipientEmail = recipientEmail; + } + + return credentialPayload; +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..b03d5bc --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "/components", + "utils": "/lib/utils" + } +} \ No newline at end of file diff --git a/components/banking-account-summary.js b/components/banking-account-summary.js deleted file mode 100644 index b2b0744..0000000 --- a/components/banking-account-summary.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import ArrowDownCircle from 'components/icons/arrow-down-circle'; -import ArrowUpCircle from 'components/icons/arrow-up-circle'; -import GlobeAlt from 'components/icons/globe-alt'; - -const BakingAccountSummary = () => ( -
-
-
- -
-
-

Total earnings

-

$20,894.30

-
-
-
-
- -
-
-

Total spending

-

$7,346.50

-
-
-
-
- -
-
-

Spending goal

-

$8,548.20

-
-
-
- ); - -export default BakingAccountSummary; diff --git a/components/button.js b/components/button.js deleted file mode 100644 index 64c0a9c..0000000 --- a/components/button.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { twMerge } from 'tailwind-merge'; - -export default function Button({ className, disabled, children, ...props }) { - const disabledClasses = 'text-white bg-blue-400 dark:bg-blue-500 cursor-not-allowed font-medium rounded-lg text-sm px-5 py-2.5 text-center'; - const defaultClasses = 'py-2 px-4 mt-5 mb-2 font-semibold text-white transition-all bg-blue-600 rounded hover:bg-blue-700 hover:-translate-y-1 duration-250'; - - return ( - - ); -} diff --git a/components/customer-info-form.js b/components/customer-info-form.js deleted file mode 100644 index d5955ec..0000000 --- a/components/customer-info-form.js +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useState } from 'react'; - -import Button from 'components/button'; -import InteractiveFormField from 'components/interactive-form-field'; -import { - textFields, - extractCredentialSubjectFromProofRequest, -} from 'utils'; - -const CustomerInfoForm = ({ - handleFormSubmit, - title, - description, - verified, - proofRequestData = null, - children -}) => { - const [isLoading, setIsLoading] = useState(false); - const [isInputValuesSet, setIsInputValuesSet] = useState( - textFields.reduce((acc, field) => { - acc[field.id] = false; - - return acc; - }, {}) - ); - - const onSubmit = async (e) => { - try { - setIsLoading(true); - await handleFormSubmit(e); - } finally { - setIsLoading(false); - } - }; - - return ( -
- {title && ( -

{title}

- )} - {description && ( -

{description}

- )} -
-
- {textFields.map((field) => ( - setIsInputValuesSet({ ...isInputValuesSet, [field.id]: value }) - } - /> - ))} -
-
- {children} -
-
- -
- ); -}; - -export default CustomerInfoForm; diff --git a/components/demo-flow.jsx b/components/demo-flow.jsx new file mode 100644 index 0000000..d798344 --- /dev/null +++ b/components/demo-flow.jsx @@ -0,0 +1,28 @@ +import { demoFlow, appartmentFlow } from 'data/demo-flow'; +import WorkFlowCard from 'components/org/workFlowCard'; + +const DemoFlow = () => ( +
+
+

Demo Flow

+
+
+ {demoFlow.map((flow, i) => ( +
+ +
+ ))} +
+ +
+ {appartmentFlow.map((flow, i) => ( +
+ +
+ ))} +
+ +
+); + +export default DemoFlow; diff --git a/components/dock-bank-helper.js b/components/dock-bank-helper.js deleted file mode 100644 index 7580c59..0000000 --- a/components/dock-bank-helper.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; - -import Button from 'components/button'; -import { useLocalStorage } from 'utils/hooks'; - -export default function Helper() { - const [isHelperOpen, setIsHelperOpen] = useState(false); - - const [holderDID, setHolderDID] = useLocalStorage('holderDID', null); - const [recipientEmail, setRecipientEmail] = useLocalStorage('recipientEmail', null); - const [formDID, setFormDID] = useState(''); - const [formRecipientEmail, setFormRecipientEmail] = useState(''); - - const actionsNeeded = !holderDID && !recipientEmail; - - // This is needed because the tailwind JIT compiler doesn't support dynamic classes - // eslint-disable-next-line no-unused-vars - const indicatorBGs = ['bg-green-500', 'bg-red-500']; - - const handleSubmit = () => { - setHolderDID(formDID); - setRecipientEmail(formRecipientEmail); - }; - - return ( - <> - - {isHelperOpen && ( -
-
-
-
-

- Helper -

- -
-
-

Current loaded holder DID:

-

{holderDID}

-
- setFormDID(e.target.value)} - /> -
-

Current loaded holder email:

-

{recipientEmail}

-
- setFormRecipientEmail(e.target.value)} - /> -
- -
-
-
- )} - - ); -} diff --git a/components/dock-bank-helper.jsx b/components/dock-bank-helper.jsx new file mode 100644 index 0000000..838078c --- /dev/null +++ b/components/dock-bank-helper.jsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { useLocalStorage } from 'hooks/hooks'; +import { validateEmail } from 'utils/validation'; +import userStore from 'store/appStore'; +import { Button } from './ui/button'; +import QrReader from './qrcode/qr-reader'; + +export default function Helper() { + const [holderDID, setHolderDID] = useLocalStorage('holderDID', ''); + const [recipientEmail, setRecipientEmail] = useLocalStorage('recipientEmail', ''); + const [emailError, setEmailError] = useState(''); + + const isHelperOpen = userStore((state) => state.isHelperOpen); + const setIsHelperOpen = userStore((state) => state.setIsHelperOpen); + const formDID = userStore((state) => state.Did); + const setFormDID = userStore((state) => state.setDid); + const formRecipientEmail = userStore((state) => state.userEmail); + const setFormRecipientEmail = userStore((state) => state.setUserEmail); + + useEffect(() => { + setFormDID(holderDID); + setFormRecipientEmail(recipientEmail); + // eslint-disable-next-line + }, [holderDID, recipientEmail]); + + const handleSubmit = () => { + if (!formDID && !formRecipientEmail) { + toast.warning('Please fill email or DID'); + return; + } + + if (formRecipientEmail && !validateEmail(formRecipientEmail)) { + setEmailError('Invalid email address'); + return; + } + + setEmailError(''); + setHolderDID(formDID); + setRecipientEmail(formRecipientEmail); + toast.success('User data saved'); + setIsHelperOpen(!isHelperOpen); + }; + + return ( + <> + + + {isHelperOpen && ( +
+
+
+

Helper

+
+ +
+
+

Input your DID or Scan it using the Qr Reader:

+ + +
+ setFormDID(e.target.value)} + /> +
+

Current loaded holder email:

+ +
+ { + setFormRecipientEmail(e.target.value); + setEmailError(''); + }} + /> +
+ {emailError &&

{emailError}

} + +
+
+
+ )} + + ); +} diff --git a/components/forms/form-field-address.jsx b/components/forms/form-field-address.jsx new file mode 100644 index 0000000..eb7a433 --- /dev/null +++ b/components/forms/form-field-address.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { Input } from 'components/ui/input'; + +/** + * @description Form Field for user info such as complete address + * @param {*} control react hook form controller + * @memberof QuotientBankForm, QuotientApplyLoanForm + * @returns React.FC Form Field + */ +const FormFieldAddress = ({ control }) => ( +
+

What is your home address?

+
+ ( + + Street Address + + + + + + )} /> + ( + + Suite (optional) + + + + + + )} /> +
+
+ ( + + Zip Code + + + + + + )} /> + ( + + City + + + + + + )} /> + ( + + State + + + + + + )} /> +
+
+); + +export default FormFieldAddress; diff --git a/components/forms/form-field-id.jsx b/components/forms/form-field-id.jsx new file mode 100644 index 0000000..0048f4b --- /dev/null +++ b/components/forms/form-field-id.jsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { format } from 'date-fns'; +import { CalendarIcon } from 'lucide-react'; +import { cn } from 'utils'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from 'components/ui/select'; +import { Input } from 'components/ui/input'; +import { Calendar } from 'components/ui/calendar'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from 'components/ui/popover'; +import { Button } from 'components/ui/button'; +import { Separator } from 'components/ui/separator'; + +/** + * @description Form Field for user info such as complete name, suffix, date of birthday (dob) + * @param {*} control react hook form controller + * @param {*} dob shows | !show BirthdayPicker comp + * @memberof QuotientBankForm, QuotientApplyLoanForm + * @returns React.FC Form Field + */ +const FormFieldNameAndBirthday = ({ control, dob = false }) => ( + <> +

Tell us your full name as it appears on your government issue ID

+
+ ( + + First Name + + + + + + )} /> + ( + + Middle Name + + + + + + )} /> + ( + + Last Name + + + + + + )} /> + ( + + Suffix (optional) + + + + )} /> +
+ {dob ? (<> + + + ) : (null)} + +); + +/** + * @description Form Field for user date of birthday (dob) + * @param {*} control react hook form controller + * @param {*} description shows default description + * @returns React.FC Form Field Calendar + */ +export const BirthdayPicker = ({ control, description = false }) => ( + ( + + {description ? ( +
+

When is your birthday?

+

(You must be at least 18 years old to open an account)

+
+ ) : ( + null + )} + Date of birth + + + + + + + + date > new Date() || date < new Date('1900-01-01')} + initialFocus + classNames={{ + caption: '', + caption_label: 'hidden' + }} + captionLayout="dropdown" + fromYear={1980} + toYear={2030} /> + + + +
+ )} /> +); + +export default FormFieldNameAndBirthday; diff --git a/components/forms/form-field-personal-contact.jsx b/components/forms/form-field-personal-contact.jsx new file mode 100644 index 0000000..d748f18 --- /dev/null +++ b/components/forms/form-field-personal-contact.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from 'components/ui/select'; +import { Input } from 'components/ui/input'; +import { Separator } from 'components/ui/separator'; + +/** + * @description Form Field for email, phone number & UsaCitizen comp + * @param {*} control react hook form controller + * @param {*} isUsaCitizen shows | !show UsaCitizen comp + * @returns React.FC Form Field + */ +const FormFieldPersonalContact = ({ control, isUsaCitizen = false }) => ( +
+

What is your contact info?

+
+ ( + + Email Address + + + + + + )} /> + ( + + Phone Number (mobile preferred) + + + + + + )} /> +
+ {isUsaCitizen ? () : (null)} +
+); + +/** + * @description Form Field for usa citizen & social security number + * @param {*} control react hook form controller + * @param {*} usaCitizen shows | !show BirthdayPicker comp + * @returns React.FC Form Field + */ +const UsaCitizen = ({ control }) => ( + <> +
+ +
+

Personal Information

+
+ ( + + U.S Citizen? + + + + )} /> + ( + + Social Security Number + + + + + + )} /> +
+ +); + +export default FormFieldPersonalContact; diff --git a/components/forms/newAccount/form-field-govId.jsx b/components/forms/newAccount/form-field-govId.jsx new file mode 100644 index 0000000..9e160e7 --- /dev/null +++ b/components/forms/newAccount/form-field-govId.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Image from 'next/image'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { Button } from 'components/ui/button'; +import WebCamPhoto from './webcam-photo'; + +/** + * @description Form Field for user passport | driver id, web cam photo + * @param {*} control react hook form controller + * @param {*} isCaptureCompleted shows WebCamPhoto comp + * @param {*} setIsCaptureCompleted setter 'isCaptureCompleted' + * @param {*} setIsUploadPoDComplete setter 'isUploadPoDComplete' (passport or driver id) + * @memberof QuotientBankForm + * @returns React.FC Form Field + */ +const FormFieldGovId = ({ control, isCaptureCompleted, setIsCaptureCompleted, setIsUploadPoDComplete }) => ( + <> + + + +); + +const PassportOrDriversId = ({ control, setIsUploadPoDComplete }) => { + const handleUploadClick = async (event) => { + event.preventDefault(); + setIsUploadPoDComplete(true); + }; + + return ( +
+ ( + +
+ Upload Passport or Drivers License + id_clarity +
+ + {field.value !== '' ? ( +
+ example_passport +
+ ) : ( +
+
+
+ upload_file +
+
+
+ +
+
+ )} +
+ +
+ )} /> +
+ ); +}; + +export default FormFieldGovId; diff --git a/components/forms/newAccount/webcam-modal.jsx b/components/forms/newAccount/webcam-modal.jsx new file mode 100644 index 0000000..bd9230b --- /dev/null +++ b/components/forms/newAccount/webcam-modal.jsx @@ -0,0 +1,69 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTrigger, +} from 'components/ui/dialog'; +import Image from 'next/image'; +import { Button } from 'components/ui/button'; +import { useState } from 'react'; + +/** + * @description Modal for mocked web cam + * @param {*} isCaptureCompleted react hook form controller + * @param {*} isCaptureCompleted react hook form controller + * @memberof WebCamPhoto + * @returns React.FC Dialog + */ +const WebCamModal = ({ isCaptureCompleted, setIsCaptureCompleted }) => { + const [isPicTaken, setIsPicTaken] = useState(false); + + const handleOnComplete = () => { + setIsPicTaken(!isPicTaken); + setTimeout(() => setIsCaptureCompleted(!isCaptureCompleted), 1000); + }; + + const handleOnOpenChanged = () => { + if (isPicTaken) setIsPicTaken(false); + }; + + return ( + + +
+ +
+
+ + {isPicTaken ? ( +

Capture Complete

+ ) : ( + <> + + + +
+ webcam_oval +
+ webcam_oval +
+
+
+

+ Position your head so that your whole face is visible in the oval. Move head left and right, up and down. +

+
+ + + )} +
+

Powered by IdentityClarity

+ id_clarity +
+
+
+ ); +}; + +export default WebCamModal; diff --git a/components/forms/newAccount/webcam-photo.jsx b/components/forms/newAccount/webcam-photo.jsx new file mode 100644 index 0000000..53d803e --- /dev/null +++ b/components/forms/newAccount/webcam-photo.jsx @@ -0,0 +1,53 @@ +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import Image from 'next/image'; +import WebCamModal from './webcam-modal'; + +/** + * @description Form Field for user web cam photo + * @param {*} control react hook form controller + * @memberof FormFieldGovId + * @returns React.FC Form Field + */ +const WebCamPhoto = ({ control, isCaptureCompleted, setIsCaptureCompleted }) => ( +
+ ( + +
+
+ Take a webcam photo for KYC check +
+
+ id_clarity +
+
+ + {isCaptureCompleted ? ( +
+ webcam_oval +
+ ) : ( +
+ background_replace + +
+ )} +
+ +
+ )} /> +
+); + +export default WebCamPhoto; diff --git a/components/forms/newLoan/form-field-car-details.jsx b/components/forms/newLoan/form-field-car-details.jsx new file mode 100644 index 0000000..e4d8878 --- /dev/null +++ b/components/forms/newLoan/form-field-car-details.jsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from 'components/ui/select'; +import { Input } from 'components/ui/input'; + +/** + * @description Form Field for loan car info + * @param {*} control react hook form controller + * @memberof QuotientApplyLoanForm + * @returns React.FC Form Field + */ +const FormFieldCarDetails = ({ control }) => ( + <> +

Tell us about the car you are wanting to purchase

+
+ ( + + Who is selling the car + + + + + + )} /> +
+ ( + + New or Used + + + + )} /> + ( + + Year + + + + + + )} /> + ( + + Mileage + + + + + + )} /> +
+ +
+ ( + + Make + + + + + + )} /> + ( + + Model + + + + + + )} /> +
+ ( + + Asking Price + + + + + + )} /> +
+ +); + +export default FormFieldCarDetails; diff --git a/components/forms/urbanscape/form-field-applicant.jsx b/components/forms/urbanscape/form-field-applicant.jsx new file mode 100644 index 0000000..e9b451a --- /dev/null +++ b/components/forms/urbanscape/form-field-applicant.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from 'components/ui/form'; +import { Input } from 'components/ui/input'; +import { BirthdayPicker } from '../form-field-id'; + +const FormFieldApplicantId = ({ control }) => ( + <> +
+ ( + + First Name + + + + + + )} /> + ( + + Last Name + + + + + + )} /> + +
+
+ ( + + Social Security # + + + + + + )} /> + ( + + Drivers License # + + + + + + )} /> + ( + + Issue State + + + + + + )} /> +
+ +); + +export default FormFieldApplicantId; diff --git a/components/forms/urbanscape/form-field-occupants.jsx b/components/forms/urbanscape/form-field-occupants.jsx new file mode 100644 index 0000000..7ca2154 --- /dev/null +++ b/components/forms/urbanscape/form-field-occupants.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage +} from 'components/ui/form'; +import { Input } from 'components/ui/input'; +import { Button } from 'components/ui/button'; +import { useFieldArray } from 'react-hook-form'; +import { Plus, Trash } from 'lucide-react'; + +const FormFieldOccupants = ({ control }) => { + const { fields, remove, append } = useFieldArray({ + control, + name: 'occupants' + }); + + const onRemoveFormItem = (index) => { + remove(index); + }; + + return ( +
+
+ {fields.map((item, index) => ( +
+ {index >= 1 ? ( +
+ +
+ ) : ( + null + )} +
+ ( + + First Name + + + + + + )} + /> + ( + + Middle Name + + + + + + )} + /> + ( + + Last Name + + + + + + )} + /> +
+
+ ))} +
+ + +
+ ); +}; + +export default FormFieldOccupants; diff --git a/components/icons/arrow-down-circle.js b/components/icons/arrow-down-circle.js deleted file mode 100644 index bf2ef77..0000000 --- a/components/icons/arrow-down-circle.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { twMerge } from 'tailwind-merge'; - -const ArrowDownCircle = ({ className }) => ( - - - -); - -export default ArrowDownCircle; diff --git a/components/icons/arrow-up-circle.js b/components/icons/arrow-up-circle.js deleted file mode 100644 index 16e24b8..0000000 --- a/components/icons/arrow-up-circle.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { twMerge } from 'tailwind-merge'; - -const ArrowUpCircle = ({ className }) => ( - - - -); - -export default ArrowUpCircle; diff --git a/components/icons/camera.js b/components/icons/camera.js deleted file mode 100644 index 49c64b8..0000000 --- a/components/icons/camera.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -const Camera = ({ width = 20, height = 20, onClick, className }) => ( - - - -); - -export default Camera; diff --git a/components/icons/censored-text.js b/components/icons/censored-text.js deleted file mode 100644 index dceafaf..0000000 --- a/components/icons/censored-text.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -const CensoredText = ({ width = 200, height = 80 }) => ( - - - - - - - - - - - -); - -export default CensoredText; diff --git a/components/icons/globe-alt.js b/components/icons/globe-alt.js deleted file mode 100644 index a92c4fa..0000000 --- a/components/icons/globe-alt.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { twMerge } from 'tailwind-merge'; - -const GlobeAlt = ({ className }) => ( - - - -); - -export default GlobeAlt; diff --git a/components/icons/upload.js b/components/icons/upload.js deleted file mode 100644 index 0ae0ee2..0000000 --- a/components/icons/upload.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -const Upload = ({ width = 20, height = 20, onClick, className }) => ( - - - -); - -export default Upload; diff --git a/components/icons/user-circle.js b/components/icons/user-circle.js deleted file mode 100644 index d03dc98..0000000 --- a/components/icons/user-circle.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -const UserCircle = ({ width = 200, height = 80 }) => ( - - - -); - -export default UserCircle; diff --git a/components/info-alert.js b/components/info-alert.js deleted file mode 100644 index 246fdef..0000000 --- a/components/info-alert.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -export default function InfoAlert({ children }) { - return ( -
- - {children} -
- ); -} diff --git a/components/interactive-form-field.js b/components/interactive-form-field.js deleted file mode 100644 index 2d07cf2..0000000 --- a/components/interactive-form-field.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -const InteractiveFormField = ({ - id, - type, - name, - placeholder, - value, - verified, - isValueSet, - setIsValueSet, -}) => { - function handleInputClick(event) { - event.preventDefault(); - event.stopPropagation(); - - if (!isValueSet) { - setIsValueSet(true); - } - } - - return ( -
-
- -
- {verified && ( -

✓ Verified

- )} -
- ); -}; - -export default InteractiveFormField; diff --git a/components/kyc-passed.js b/components/kyc-passed.js deleted file mode 100644 index 9543571..0000000 --- a/components/kyc-passed.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { - kycSteps, -} from 'utils'; - -const KYCPassed = () => ( -
-

KYC check passed

- {kycSteps.map((step) => ( -
- -

{step.label}

-
- ))} -

Verified by IDProvider

-
-); - -export default KYCPassed; diff --git a/components/org/equinet/credential-details.jsx b/components/org/equinet/credential-details.jsx new file mode 100644 index 0000000..f871b89 --- /dev/null +++ b/components/org/equinet/credential-details.jsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { useRevoke } from 'hooks/useRevoke'; +import { Loader2, Check, ShieldX } from 'lucide-react'; +import { Button } from 'components/ui/button'; + +/** + * @description Equinet Credentials comp + * @param {*} setData push a new object to data + * @memberof EquinetPage + * @returns React.FC + */ +const CredentialDetails = ({ setData }) => { + const [credentials, setCredentials] = useState([ + { + title: 'Credit Score of 706', + description: 'EquiNET', + status: 'Out of Date' + }, + { + title: 'Credit Score of 625', + description: 'EquiNET', + status: 'Good' + }, + ]); + + const { + loadingRevokation, + handleRevoke + } = useRevoke(); + + function formatDate() { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + return new Date().toLocaleDateString('en-US', options); + } + + async function handleAnim() { + setTimeout(() => { + setData((oldData) => [...oldData, + { + action: 'Credit Score of 625', + status: 'Revoked', + date: formatDate() + }, + { + action: 'Credit Score of 615', + status: 'Good', + date: formatDate() + }]); + setCredentials(() => [ + { + title: 'Credit Score of 706', + description: 'EquiNET', + status: 'Out of Date' + }, + { + title: 'Credit Score of 625', + description: 'EquiNET', + status: 'Revoked' + }, + { + title: 'Credit Score of 615', + description: 'EquiNET', + status: 'Good' + }, + ]); + }, 1000); + } + + const onRevokeCredential = async () => { + await handleRevoke(); + await handleAnim(); + }; + + return ( +
+

Credential Details

+
+ {credentials.map((value, index) => ( +
+
+

{value.title}

+ +

{value.description}

+
+ {value.status === 'Good' ? () : ()} +
+ ))} +
+ + +
+ ); +}; + +export default CredentialDetails; diff --git a/components/org/equinet/dataTable/columns.js b/components/org/equinet/dataTable/columns.js new file mode 100644 index 0000000..efdef0f --- /dev/null +++ b/components/org/equinet/dataTable/columns.js @@ -0,0 +1,29 @@ +import { Badge } from 'components/ui/badge'; + +export const columns = [ + { + accessorKey: 'action', + header: 'Action', + }, + { + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => { + const value = row.getValue('status'); + return ( + <> + {value === 'Good' ? ( + {value} + ) : ( + {value} + )} + + + ); + } + }, + { + accessorKey: 'date', + header: 'Date', + }, +]; diff --git a/components/org/equinet/dataTable/data-table.jsx b/components/org/equinet/dataTable/data-table.jsx new file mode 100644 index 0000000..9130365 --- /dev/null +++ b/components/org/equinet/dataTable/data-table.jsx @@ -0,0 +1,90 @@ +import { + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from 'components/ui/table'; +import { Button } from 'components/ui/button'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +export function DataTable({ + columns, + data, +}) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+

Page 1 of 10

+
+ + +
+
+
+ ); +} diff --git a/components/org/equinet/dataTable/index.jsx b/components/org/equinet/dataTable/index.jsx new file mode 100644 index 0000000..01dd2c3 --- /dev/null +++ b/components/org/equinet/dataTable/index.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Input } from 'components/ui/input'; +import { Search } from 'lucide-react'; +import { columns } from './columns'; +import { DataTable } from './data-table'; + +function EquinetTable({ data }) { + return ( +
+
+

History

+
+ + +
+
+ +
+ ); +} + +export default EquinetTable; diff --git a/components/org/equinet/sidebar.jsx b/components/org/equinet/sidebar.jsx new file mode 100644 index 0000000..4c46be5 --- /dev/null +++ b/components/org/equinet/sidebar.jsx @@ -0,0 +1,74 @@ +import { cn } from 'utils'; +import { Button } from 'components/ui/button'; +import { CircleUserRound, LogOut, ChevronDown, ChevronLeft } from 'lucide-react'; +import Image from 'next/image'; + +import Link from 'next/link'; + +/** + * @description Equinet sidebar menu. + * @param {*} className custom style + * @returns React.FC Sidebar + */ +export function Sidebar({ className }) { + return ( +
+
+
+
+
+ + + +
+ equinetlogo +
+
+
+ + + + + + + + + +
+
+
+
+
+
+ +

John Doe

+
+ + + +
+
+
+
+ ); +} diff --git a/components/org/organizationCard.jsx b/components/org/organizationCard.jsx new file mode 100644 index 0000000..885929d --- /dev/null +++ b/components/org/organizationCard.jsx @@ -0,0 +1,53 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +export default function OrganizationCard({ org }) { + return ( + <> +
+

+ {org.label} +

+
+ orglogo +
+
+
+
+

+ {org.description} +

+
+ +
+ {org.name === 'Quotient' ? ( + <> +
+ + + +
+
+ + + +
+ + ) : ( + org.button && org.button === true && ( + + + + ) + )} +
+ + + ); +} diff --git a/components/org/quotient/Header.jsx b/components/org/quotient/Header.jsx new file mode 100644 index 0000000..8b967ce --- /dev/null +++ b/components/org/quotient/Header.jsx @@ -0,0 +1,27 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { ChevronLeft } from 'lucide-react'; + +/** + * @description Quotient header comp + * @memberof QuotientBankForm, QuotientApplyLoanForm + * @returns React.FC + */ +const Header = () => ( +
+
+ + + +
+ quotient-logo +
+); + +export default Header; diff --git a/components/org/quotient/bank-credentials.jsx b/components/org/quotient/bank-credentials.jsx new file mode 100644 index 0000000..39aa6ab --- /dev/null +++ b/components/org/quotient/bank-credentials.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Check } from 'lucide-react'; +import qrCodeStore from 'store/qrCodeStore'; + +const credentials = { + biometric: { + title: 'Biometric Check', + description: 'ForSur' + }, + loan: { + title: 'Bank Customer Identity', + description: 'Quotient Credit Union' + }, + credit: { + title: 'Credit Score', + description: 'Equinet' + }, +}; + +const CredentialCards = ({ onlyCreditScore = false, required = false }) => { + const verified = qrCodeStore((state) => state.verified); + + return ( + <> +
+

{!required ? 'Offered credentials:' : 'Required credentials:'}

+
+
+ + { + onlyCreditScore ? +
+
+

{credentials.credit.title}

+

{credentials.credit.description}

+
+ {verified && } +
+ : + Object.entries(credentials).map(([key, value], index) => ( +
+
+

{value.title}

+

{value.description}

+
+ {verified && } +
+ )) + } +
+ + ); +}; + +export default CredentialCards; diff --git a/components/org/quotient/loading-modal.jsx b/components/org/quotient/loading-modal.jsx new file mode 100644 index 0000000..fed7c85 --- /dev/null +++ b/components/org/quotient/loading-modal.jsx @@ -0,0 +1,25 @@ +import { Loader2 } from 'lucide-react'; +import Image from 'next/image'; +import { + Dialog, + DialogContent +} from 'components/ui/dialog'; + +/** + * @description Quotient modal for loading while issuing credentials + * @param {*} isLoading controls opening of dialog + * @memberof QuotientBankForm + * @returns React.FC Dialog + */ +const LoadingModal = ({ isLoading }) => ( + + +
+ +

Creating Bank account...

+
+
+
+); + +export default LoadingModal; diff --git a/components/org/quotient/quotient-success.jsx b/components/org/quotient/quotient-success.jsx new file mode 100644 index 0000000..85e86aa --- /dev/null +++ b/components/org/quotient/quotient-success.jsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import QrCodeAuthentication from 'components/qrcode/qr-auth'; +import qrCodeVerificationData from 'data/qrcode-text-data'; +import useQrCode from 'hooks/useQrCode'; +import { Separator } from '../../ui/separator'; + +const textFields = { + thanks: 'Thank you for trusting Quotient with your financial needs. We are thankful for you. Quotient is a part of IdentityClarity\'s partner ecosystem called Clarity Partners.', + benefits: 'As a customer of Quotient Credit Union, you are able to receive valuable benefits from other Clarity Partners by using verified information stored in Quotient\'s mobile banking app. Using your mobile banking app to share data with a Clarity Partner is quick, secure, and private.', + instructions: 'Scan the QR code on the right to get started.' +}; + +/** + * @description Quotient Success for open a new account. + * @param {*} title title for comp + * @memberof QuotientBankForm, QuotientApplyLoanForm + * @returns React.FC Form Field + */ +const QuotientSuccess = ({ title, proofTemplateId, showQrcode = true }) => { + const { refetch } = useQrCode({ proofTemplateId }); + + useEffect(() => { + refetch(); + window.scrollTo(0, 0); + // eslint-disable-next-line + }, []); + + return ( +
+
+

{title}

+
+
+

{textFields.thanks}

+ + +
+ clarity_partners +
+
+ + +

+ {textFields.benefits} +

+

+ Download the Quotient Mobile App by clicking + + this link + + +

+

{textFields.instructions}

+
+ {showQrcode && +
+ + +
+ } +
+
+
); +}; + +export default QuotientSuccess; diff --git a/components/org/urbanscape/header.jsx b/components/org/urbanscape/header.jsx new file mode 100644 index 0000000..14744c5 --- /dev/null +++ b/components/org/urbanscape/header.jsx @@ -0,0 +1,37 @@ +import { ChevronLeft } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; + +/** + * @description Urbanscape header comp + * @memberof UrbanScapePage, UrbanScapeSuccess + * @returns React.FC + */ +const Header = () => ( +
+
+
+ + + +
+ urbanscape-logo + +
+
+ partnersgrey +
+
+); + +export default Header; diff --git a/components/org/urbanscape/urbanscape-success.jsx b/components/org/urbanscape/urbanscape-success.jsx new file mode 100644 index 0000000..1502456 --- /dev/null +++ b/components/org/urbanscape/urbanscape-success.jsx @@ -0,0 +1,97 @@ +import React, { useEffect } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { CheckCircle2 } from 'lucide-react'; +import { Button } from 'components/ui/button'; +import { PROOFT_TEMPLATES_IDS } from 'utils/constants'; +import QrCodeAuthentication from 'components/qrcode/qr-auth'; +import qrCodeVerificationData from 'data/qrcode-text-data'; +import useQrCode from 'hooks/useQrCode'; +import { Separator } from '../../ui/separator'; + +/** + * @description Urbanscape Success for approved application for appartment. + * @memberof UrbanScapePage + * @returns React.FC + */ +const UrbanscapeSuccess = () => { + const proofTemplateId = PROOFT_TEMPLATES_IDS.URBANSCAPE_CREDITSCORE; + const { refetch } = useQrCode({ proofTemplateId }); + + useEffect(() => { + setTimeout(() => { + refetch(); + }, 1000); + window.scrollTo(0, 0); + // eslint-disable-next-line + }, []); + + return ( +
+
+

You have been approved!

+

To reserve your new appartment home, please pay total amount due now.

+
+
+
+
+
+

2 bedroom 2 bath with city view

+ +
+
+
+

$2,550

+

per month

+
+

Includes use of all common area, pool, gym and parks.

+
+
+
+
+

Deposit

+ +
+
+
+

$1,440

+

one time

+
+

We have a special offer for our applicants that have good credit. Provide to us a verified credential with a credit score over 700 and get your deposit waived!

+

This is a $1,440.00 value.

+

Scan the QR code on the right with your mobile app to see if you qualify.

+
+
+
+

TOTAL DUE NOW:

+

$3,990

+ +
+ +
+

{'Urbanscape is a part of the IdentityClarity\'s ecosystem called clarity partners.'}

+ + +
+ clarity_partners +
+
+ +
+
+
+ +
+
+
+ ); +}; + +export default UrbanscapeSuccess; diff --git a/components/org/workFlowCard.jsx b/components/org/workFlowCard.jsx new file mode 100644 index 0000000..8b69878 --- /dev/null +++ b/components/org/workFlowCard.jsx @@ -0,0 +1,30 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { ChevronsRight } from 'lucide-react'; + +export default function WorkFlowCard({ flow, arrow = false }) { + return ( +
+ +
+
orgLogo
+ {flow.secondLogo &&
orgLogo
} +
+ + +
+
+

{flow.title}

+
+ {arrow && ( +
+ +
+ )} +
+ + +
+ + ); +} diff --git a/components/page-layout.js b/components/page-layout.js deleted file mode 100644 index 0f48014..0000000 --- a/components/page-layout.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import Sidebar from 'components/sidebar'; -import clsx from 'clsx'; - -export default function PageLayout({ title, withSidebar = true, children }) { - const mainClasses = clsx( - 'mb-6 lg:w-[75%] xl:w-[80%] 2xl:w-[85%]', - withSidebar ? 'ml-auto' : 'mx-auto' - ); - const titleWrapperClasses = clsx( - 'flex items-center px-6 space-x-4 2xl:container', - withSidebar ? 'justify-between' : 'justify-center' - ); - - return ( - <> - {withSidebar && ()} -
- {title && ( -
-
- -
-
-
- )} -
- {children} -
-
- - ); -} diff --git a/components/page-layout.jsx b/components/page-layout.jsx new file mode 100644 index 0000000..19ac780 --- /dev/null +++ b/components/page-layout.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import 'react-toastify/dist/ReactToastify.css'; +import { ToastContainer } from 'react-toastify'; + +export default function PageLayout({ children }) { + return ( + <> + +
+ {children} +
+ + ); +} diff --git a/components/page-title.js b/components/page-title.js deleted file mode 100644 index fe294ad..0000000 --- a/components/page-title.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export default function PageTitle({ children }) { - return ( -

{children}

- ); -} diff --git a/components/partners/partner-card.jsx b/components/partners/partner-card.jsx new file mode 100644 index 0000000..88f67a2 --- /dev/null +++ b/components/partners/partner-card.jsx @@ -0,0 +1,21 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +const PartnerCard = ({ partner }) => ( +
+ +
+ partnerLogo +
+ + +
+

+ {partner.short ? partner.short : partner.description} +

+
+ +
+ ); + +export default PartnerCard; diff --git a/components/partners/partner-navbar.jsx b/components/partners/partner-navbar.jsx new file mode 100644 index 0000000..c07623c --- /dev/null +++ b/components/partners/partner-navbar.jsx @@ -0,0 +1,32 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { ChevronLeft } from 'lucide-react'; + +const PartnerNavbar = () => ( + + <> +
+
+
+ + + +
+
+
+ partners-logo +
+
+
+
+ + + +); + +export default PartnerNavbar; diff --git a/components/partners/partnerContent.jsx b/components/partners/partnerContent.jsx new file mode 100644 index 0000000..4e3cef4 --- /dev/null +++ b/components/partners/partnerContent.jsx @@ -0,0 +1,38 @@ +import Link from 'next/link'; +import { Button } from 'components/ui/button'; + +const PartnerContent = ({ currentPartner }) => ( + <> + {currentPartner?.subtitle &&
+

{currentPartner.subtitle}

+
} +
+

{currentPartner?.description}

+
+ + {currentPartner.paragraph !== undefined &&
+

{currentPartner.paragraph}

+
} + + {currentPartner.links && currentPartner.links.length > 0 && ( +
+ {currentPartner.links && currentPartner.links.map((link) => ( +
+ {link.url !== undefined ? ( + + ) : ( +
+ )} +
+ ))} +
+ )} + +); + +export default PartnerContent; diff --git a/components/partners/partners-header.jsx b/components/partners/partners-header.jsx new file mode 100644 index 0000000..798d753 --- /dev/null +++ b/components/partners/partners-header.jsx @@ -0,0 +1,45 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { ChevronLeft } from 'lucide-react'; +import { Separator } from 'components/ui/separator'; + +const PartnersHeader = () => ( + + <> +
+
+
+ + + +
+
+
+ partners-logo +
+
+ +
+

+ Welcome to the Identity Clarity ecosystem clarity partners. +

+
+
+

+ Join the privacy conscious ecosystem of partners providing valuable + services to all of our members. +

+
+
+
+ + + +); + +export default PartnersHeader; diff --git a/components/qr.js b/components/qr.js deleted file mode 100644 index d19057a..0000000 --- a/components/qr.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import QRCode from 'react-qr-code'; - -export default function QRDisplay({ value }) { - return value ? ( -
-
- -
- -
- ) : ( -
-
- Loading -
-
- ); -} diff --git a/components/qrcode/qr-auth.jsx b/components/qrcode/qr-auth.jsx new file mode 100644 index 0000000..d2a5b71 --- /dev/null +++ b/components/qrcode/qr-auth.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Separator } from 'components/ui/separator'; +import VerifyQrCode from 'components/qrcode/verify-qr-code'; +import qrCodeStore from 'store/qrCodeStore'; +import useQrCode from 'hooks/useQrCode'; +import { RefreshCw } from 'lucide-react'; +import { useVerifyProof } from 'hooks/useVerifyProof'; +import CredentialCards from '../org/quotient/bank-credentials'; + +const QrCodeAuthentication = ({ proofTemplateId, title = '', qrText = '', qrTextAfter = '', onlyCreditScore = false, required = true }) => { + const verified = qrCodeStore((state) => state.verified); + const { refetch } = useQrCode({ proofTemplateId }); + useVerifyProof(); + + return ( +
+ {(title !== null && title !== '') &&
+

{title}

+ +
+ } + {!verified ? ( + <> + {(qrText !== null && qrText !== '') && ( +
+

{qrText}

+
+ )} + + + + {(qrTextAfter !== null && qrTextAfter !== '') && ( +
+

{qrTextAfter}

+ +
+ )} + + ) : +
+ refetch()} /> +
+ } + +
+ +
+
+ ); +}; + +export default QrCodeAuthentication; diff --git a/components/qrcode/qr-generator.jsx b/components/qrcode/qr-generator.jsx new file mode 100644 index 0000000..5cda1f3 --- /dev/null +++ b/components/qrcode/qr-generator.jsx @@ -0,0 +1,24 @@ +import { useQRCode } from 'next-qrcode'; + +export const QRCodeGenerator = ({ url }) => { + const { Canvas } = useQRCode(); + const qrCodeSize = 195; + + return ( +
+ +
+ ); +}; diff --git a/components/qrcode/qr-reader.jsx b/components/qrcode/qr-reader.jsx new file mode 100644 index 0000000..416b376 --- /dev/null +++ b/components/qrcode/qr-reader.jsx @@ -0,0 +1,59 @@ +import { Html5QrcodeScanner, Html5QrcodeScanType } from 'html5-qrcode'; +import { Fingerprint, Webcam } from 'lucide-react'; +import { toast } from 'react-toastify'; + +export default function QrReader({ setDID }) { + let scanner; + + function startCamera() { + scanner = new Html5QrcodeScanner( + 'reader', + { + fps: 8, + qrbox: { + width: 250, + height: 250, + }, + experimentalFeatures: { + useBarCodeDetectorIfSupported: true, + }, + rememberLastUsedCamera: true, + supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA], + }, + false + ); + if (scanner) { + scanner.clear(); + scanner.render(onScanSuccess, onScanFailure); + } + } + + async function onScanSuccess(decodedResult) { + scanner.clear(); + const result = decodedResult; + + console.log(`scanned result: ${result}`); + setDID(result); + toast.success('QR Code Scanned Successfully'); + return result; + } + + function onScanFailure(error) { + console.warn(`Code scan error = ${error}`); + } + + return ( +
+
+
+ +
+ + +
+
+
+ ); +} diff --git a/components/qrcode/verify-qr-code.jsx b/components/qrcode/verify-qr-code.jsx new file mode 100644 index 0000000..9aec133 --- /dev/null +++ b/components/qrcode/verify-qr-code.jsx @@ -0,0 +1,50 @@ +import { useEffect } from 'react'; +import useQrCode from 'hooks/useQrCode'; +import { QRCodeGenerator } from 'components/qrcode/qr-generator'; +import { toast } from 'sonner'; +import qrCodeStore from 'store/qrCodeStore'; +import { Loader2, RefreshCw } from 'lucide-react'; + +const VerifyQrCode = ({ proofTemplateId }) => { + const qrCodeUrl = qrCodeStore((state) => state.qrCodeUrl); + const verified = qrCodeStore((state) => state.verified); + const isLoading = qrCodeStore((state) => state.isLoading); + const verificationError = qrCodeStore((state) => state.verificationError); + const { refetch } = useQrCode({ proofTemplateId }); + + useEffect(() => { + if (verified === true) { + toast.success('Verification Success!'); + return; + } + + if (verificationError) { + console.log('refetching new Qr code...'); + refetch(); + } + }, [verified, refetch, verificationError]); + + return ( + <> + {isLoading ? ( +
+
+ +

Generating Qr code..

+
+
+ ) : ( + qrCodeUrl !== '' && !verified ? ( +
+ + refetch()} /> +
+ ) :
+ refetch()} /> +
+ )} + + ); +}; + +export default VerifyQrCode; diff --git a/components/require-proof.js b/components/require-proof.js deleted file mode 100644 index 2e41b4a..0000000 --- a/components/require-proof.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; - -import QRDisplay from 'components/qr'; - -const CHECK_PROOF_INTERVAL = 5000; - -export default function RequireProof({ onPresentedProof, type }) { - const [proofRequest, setProofRequest] = useState(); - - async function getProofRequest() { - const { data } = await axios.get(`/api/proof-request?type=${type}`); - - if (!proofRequest) { - setProofRequest(data); - setTimeout(() => checkProofRequest(data), CHECK_PROOF_INTERVAL); - } - } - - async function checkProofRequest(request) { - const { data } = await axios.get(`/api/proof-request?id=${request.id}`); - const { verified } = data; - - if (verified) { - onPresentedProof(data); - } else { - setTimeout(() => checkProofRequest(request), CHECK_PROOF_INTERVAL); - } - } - - useEffect(() => { - if (!proofRequest) { - getProofRequest(); - } - }, [proofRequest]); - - return proofRequest ? ( - - ) : ( - - ); -} diff --git a/components/sidebar.js b/components/sidebar.js deleted file mode 100644 index d3695d2..0000000 --- a/components/sidebar.js +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import clsx from 'clsx'; - -import { useLocalStorage } from 'utils/hooks'; -import { extractCredentialSubjectFromProofRequest } from 'utils'; -import { BANK_NAME } from 'utils/constants'; - -const navLinks = [ - { - title: 'Dashboard', - link: '/dashboard', - target: '_self', - }, - { - title: 'Credit Card', - link: '/credit-card', - target: '_self', - }, - { - title: 'Reward Program', - link: '/rewards-program', - target: '_blank', - }, - { - title: 'Customer Service', - link: '/customer-service', - target: '_self', - }, -]; - -const Sidebar = () => { - const router = useRouter(); - const [userData] = useLocalStorage('userData', null); - - const { name } = extractCredentialSubjectFromProofRequest( - userData, - 'CustomerCredential' - ) || {}; - - const getLinkClassName = (link) => clsx( - 'relative px-3 py-2 flex items-center space-x-4 rounded-md transition duration-300', - link.link === router.pathname ? 'text-gray-600 bg-gray-200' : '', - ); - - return ( - - ); -}; - -export default Sidebar; diff --git a/components/ui/badge.jsx b/components/ui/badge.jsx new file mode 100644 index 0000000..fc70b43 --- /dev/null +++ b/components/ui/badge.jsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { cva } from 'class-variance-authority'; + +import { cn } from 'lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: + 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +function Badge({ + className, + variant, + ...props +}) { + return (
); +} + +export { Badge, badgeVariants }; diff --git a/components/ui/button.jsx b/components/ui/button.jsx new file mode 100644 index 0000000..0120ecc --- /dev/null +++ b/components/ui/button.jsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva } from 'class-variance-authority'; +import { cn } from 'utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + () + ); +}); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/components/ui/calendar.jsx b/components/ui/calendar.jsx new file mode 100644 index 0000000..8e13f63 --- /dev/null +++ b/components/ui/calendar.jsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { DayPicker } from 'react-day-picker'; + +import { cn } from 'utils'; +import { buttonVariants } from 'components/ui/button'; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}) { + return ( + ( , + IconRight: ({ ...props }) => , + }} + {...props} />) + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/components/ui/dialog.jsx b/components/ui/dialog.jsx new file mode 100644 index 0000000..9c93209 --- /dev/null +++ b/components/ui/dialog.jsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from 'utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/components/ui/form.jsx b/components/ui/form.jsx new file mode 100644 index 0000000..00a86fb --- /dev/null +++ b/components/ui/form.jsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { Controller, FormProvider, useFormContext } from 'react-hook-form'; + +import { cn } from 'utils'; +import { Label } from 'components/ui/label'; + +const Form = FormProvider; + +const FormFieldContext = React.createContext({}); + +const FormField = ( + { + ...props + } +) => ( + ( + + ) +); + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +const FormItemContext = React.createContext({}); + +const FormItem = React.forwardRef(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + ( +
+ ) + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( + (