├── client
├── src
│ ├── App.css
│ ├── assets
│ │ └── scss
│ │ │ ├── main.scss
│ │ │ ├── components
│ │ │ ├── _all.scss
│ │ │ ├── _modal.scss
│ │ │ ├── _footer.scss
│ │ │ ├── _signlist.scss
│ │ │ ├── _behindTheScenes.scss
│ │ │ ├── _header.scss
│ │ │ ├── _successPage.scss
│ │ │ ├── _popup.scss
│ │ │ ├── _pages.scss
│ │ │ ├── _login.scss
│ │ │ ├── _home.scss
│ │ │ └── _form.scss
│ │ │ └── variables
│ │ │ └── _variables.scss
│ ├── api
│ │ ├── request.js
│ │ └── apiHelper.js
│ ├── index.js
│ ├── pages
│ │ ├── UseCaseIndex.js
│ │ ├── PageNotFound.js
│ │ ├── ErrorPage.js
│ │ ├── SmallBusinessLoan
│ │ │ ├── SubmittedLoan.js
│ │ │ └── SmallBusinessLoan.js
│ │ ├── TrafficTicket
│ │ │ ├── TrafficTicket.js
│ │ │ ├── WitnessStatement.js
│ │ │ └── SubmittedTrafficTicket.js
│ │ ├── Success.js
│ │ ├── Home.js
│ │ ├── Login.js
│ │ └── PassPort
│ │ │ ├── PassportSign.js
│ │ │ └── Passport.js
│ ├── setupProxy.js
│ ├── index.css
│ ├── components
│ │ ├── Popup.js
│ │ ├── UserProfile.js
│ │ ├── Footer.js
│ │ ├── Card.js
│ │ ├── BehindTheScenes.js
│ │ ├── Header.js
│ │ ├── RequireAuth.js
│ │ ├── Modal.js
│ │ └── Form.js
│ └── App.js
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── .gitignore
└── package.json
├── server
├── docusign
│ ├── pdf
│ │ ├── sign1.pdf
│ │ ├── LoanChecklist.pdf
│ │ ├── TrafficTicket.pdf
│ │ ├── PassportApplication.pdf
│ │ ├── SmallBusinessLoanApp.pdf
│ │ └── PoliceWitnessStatement.pdf
│ ├── workflow.js
│ ├── templates.js
│ ├── envelopes
│ │ ├── makeSmsEnvelope.js
│ │ ├── makeLoanApplication.js
│ │ └── makeTrafficTicket.js
│ ├── envelopByTemplate.js
│ ├── envelope.js
│ └── useTemplate.js
├── assets
│ ├── public
│ │ └── img
│ │ │ ├── hero.png
│ │ │ ├── passport.png
│ │ │ ├── cody_avatar.png
│ │ │ ├── sage_avatar.png
│ │ │ ├── blaire_avatar.png
│ │ │ ├── millie_avatar.png
│ │ │ ├── paula_avatar.png
│ │ │ ├── default_avatar.png
│ │ │ ├── small_business.png
│ │ │ ├── traffic_ticket.png
│ │ │ ├── down_caret.svg
│ │ │ ├── up_caret.svg
│ │ │ ├── check.svg
│ │ │ └── logo.svg
│ └── errorText.json
├── routes
│ ├── passportRouter.js
│ ├── loanRouter.js
│ ├── jwtRouter.js
│ ├── trafficRouter.js
│ └── templateRouter.js
├── utils
│ └── appError.js
├── .gitignore
├── package.json
├── controllers
│ ├── passportController.js
│ ├── loanController.js
│ ├── jwtController.js
│ ├── trafficController.js
│ └── templateContraoller.js
└── server.js
├── .gitignore
├── .env
├── README.md
└── private.key
/client/src/App.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/client/src/assets/scss/main.scss:
--------------------------------------------------------------------------------
1 | @import 'components/all';
2 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/client/public/favicon.ico
--------------------------------------------------------------------------------
/server/docusign/pdf/sign1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/sign1.pdf
--------------------------------------------------------------------------------
/server/assets/public/img/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/hero.png
--------------------------------------------------------------------------------
/server/assets/public/img/passport.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/passport.png
--------------------------------------------------------------------------------
/server/docusign/pdf/LoanChecklist.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/LoanChecklist.pdf
--------------------------------------------------------------------------------
/server/docusign/pdf/TrafficTicket.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/TrafficTicket.pdf
--------------------------------------------------------------------------------
/server/assets/public/img/cody_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/cody_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/sage_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/sage_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/blaire_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/blaire_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/millie_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/millie_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/paula_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/paula_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/default_avatar.png
--------------------------------------------------------------------------------
/server/assets/public/img/small_business.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/small_business.png
--------------------------------------------------------------------------------
/server/assets/public/img/traffic_ticket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/assets/public/img/traffic_ticket.png
--------------------------------------------------------------------------------
/server/docusign/pdf/PassportApplication.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/PassportApplication.pdf
--------------------------------------------------------------------------------
/server/docusign/pdf/SmallBusinessLoanApp.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/SmallBusinessLoanApp.pdf
--------------------------------------------------------------------------------
/server/docusign/pdf/PoliceWitnessStatement.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengbid/docusign_demo/main/server/docusign/pdf/PoliceWitnessStatement.pdf
--------------------------------------------------------------------------------
/client/src/api/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const instance = axios.create({
4 | baseURL: '/api'
5 | });
6 |
7 | export default instance
--------------------------------------------------------------------------------
/server/assets/public/img/down_caret.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/server/assets/public/img/up_caret.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/server/routes/passportRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const passportController = require('../controllers/passportController');
4 |
5 | router.post('/', passportController.createController);
6 |
7 | module.exports = router;
8 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/server/assets/public/img/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/src/pages/UseCaseIndex.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from 'react-router-dom';
3 |
4 | // This page enables child pages to be displayed for use cases
5 | // with multiple pages.
6 | function UseCaseIndex() {
7 | return ;
8 | }
9 |
10 | export default UseCaseIndex;
11 |
--------------------------------------------------------------------------------
/server/utils/appError.js:
--------------------------------------------------------------------------------
1 | class AppError extends Error {
2 | constructor(statusCode, message) {
3 | super(message);
4 | this.statusCode = statusCode;
5 | // this.isOperational = true;
6 |
7 | Error.captureStackTrace(this, this.constructor);
8 | }
9 | }
10 |
11 | module.exports = AppError;
12 |
--------------------------------------------------------------------------------
/client/src/pages/PageNotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function PageNotFound({ text }) {
4 | return (
5 |
6 |
7 |
{text.title}
8 |
9 |
10 | );
11 | }
12 |
13 | export default PageNotFound;
14 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/server/routes/loanRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | createController,
5 | submitLoanController,
6 | } = require('../controllers/loanController');
7 |
8 | router.post('/', createController);
9 | router.get('/submitted', submitLoanController);
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/client/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const {createProxyMiddleware}=require("http-proxy-middleware")
2 |
3 | module.exports = function(app){
4 | app.use(
5 | createProxyMiddleware(
6 | "/api",
7 | {
8 | target: "http://192.168.10.147:8085",
9 | changeOrigin: true,
10 | pathRewrite: { '^/api': '' },
11 | }
12 | )
13 | )
14 | }
--------------------------------------------------------------------------------
/server/routes/jwtRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const jwtController = require('../controllers/jwtController');
4 |
5 | router.get('/isLoggedIn', jwtController.isLoggedIn);
6 | router.get('/login', jwtController.login);
7 | router.get('/logout', jwtController.logout);
8 | router.get('/getUserInfo', jwtController.getUserInfo);
9 |
10 | module.exports = router;
11 |
--------------------------------------------------------------------------------
/client/.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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/server/routes/trafficRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | createController,
5 | smsTrafficController,
6 | submitTrafficController,
7 | } = require('../controllers/trafficController');
8 |
9 | router.post('/', createController);
10 | router.post('/sms', smsTrafficController);
11 | router.get('/submitted', submitTrafficController);
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/.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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | # .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # private.key
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/Popup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import parse from 'html-react-parser';
3 |
4 | function Popup({ text, handleClose }) {
5 | const description = parse(text.description);
6 |
7 | return (
8 |
9 |
10 |
11 |
12 | ×
13 |
14 |
{text.title}
15 |
16 |
17 |
{description}
18 |
19 |
20 | );
21 | }
22 |
23 | export default Popup;
24 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_all.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 | @use 'pages';
3 | @use 'form';
4 | @use 'behindTheScenes';
5 | @use 'popup';
6 | @use 'header';
7 | @use 'footer';
8 | @use 'home';
9 | @use 'successPage';
10 | @use 'login';
11 | @use 'signlist';
12 | @use 'modal';
13 |
14 | @import url('https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:wght@500&display=swap');
15 |
16 | body {
17 | background-color: v.$bgColor;
18 | font-size: v.$bodyTextSize;
19 | // font-family: 'Raleway', sans-serif;
20 | font-weight: 500;
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/components/UserProfile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function UserProfile({
4 | buttonName,
5 | avatarUrl,
6 | userRoleName,
7 | includeMoreInfo,
8 | popUpOpen,
9 | setPopUpOpen,
10 | }) {
11 | return (
12 |
13 |

14 |
{userRoleName}
15 | {includeMoreInfo ? (
16 |
23 | ) : (
24 | // Empty div to take up space to keep form height the same.
25 |
26 | )}
27 |
28 | );
29 | }
30 |
31 | export default UserProfile;
32 |
--------------------------------------------------------------------------------
/client/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Footer({ text }) {
4 | return (
5 |
6 |
{text.title}
7 | {text.description}
8 |
9 |
27 |
28 | );
29 | }
30 |
31 | export default Footer;
32 |
--------------------------------------------------------------------------------
/client/src/pages/ErrorPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useLocation, useNavigate } from 'react-router-dom';
3 | import parse from 'html-react-parser';
4 |
5 | function ErrorPage() {
6 | let navigate = useNavigate();
7 | const { state } = useLocation();
8 | const defaultTitle = 'Unknown error occurred';
9 | const backLinkName = 'Back to Home';
10 |
11 | return (
12 |
13 |
14 | {state && state.title ? (
15 | <>
16 |
{state.title}
17 | {parse(state.description)}
18 | >
19 | ) : (
20 | {defaultTitle}
21 | )}
22 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default ErrorPage;
34 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "client": "cd ../ && npm start --prefix client",
9 | "server": "nodemon server.js --ignore client",
10 | "dev": "concurrently \"npm run server\" \"npm run client\""
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "http://localhost"
15 | },
16 | "author": "Emily Wang",
17 | "license": "MIT",
18 | "dependencies": {
19 | "@types/docusign-esign": "^5.6.2",
20 | "axios": "^0.25.0",
21 | "body-parser": "^1.19.2",
22 | "concurrently": "^7.0.0",
23 | "cookie-parser": "^1.4.6",
24 | "cookie-session": "^2.0.0",
25 | "dayjs": "^1.10.7",
26 | "docusign-esign": "^5.15.0",
27 | "dotenv": "^14.3.2",
28 | "express": "^4.17.3",
29 | "helmet": "^5.0.2"
30 | },
31 | "devDependencies": {
32 | "nodemon": "^2.0.15"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import parse from 'html-react-parser';
3 | import { Link } from 'react-router-dom';
4 |
5 | function Card({ cardType, buttonType, iconUrl, title, featureList, linkTo }) {
6 | const buttonName = '开始';
7 | const featureListTitle = '功能点:';
8 |
9 | return (
10 |
11 |
12 |

13 |
14 |
{title}
15 |
{featureListTitle}
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default Card;
30 |
--------------------------------------------------------------------------------
/client/src/components/BehindTheScenes.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import parse from 'html-react-parser';
3 |
4 | function BehindTheScenes({ title, description }) {
5 | const parsedDescription = parse(description);
6 | const upCaretUrl = '/assets/img/up_caret.svg';
7 | const downCaretUrl = '/assets/img/down_caret.svg';
8 | const [showDesc, setShowDesc] = useState(false);
9 |
10 | return (
11 |
12 |
{
15 | setShowDesc(!showDesc);
16 | }}
17 | >
18 | {showDesc ? (
19 |

20 | ) : (
21 |

22 | )}
23 |
{title}
24 |
25 | {showDesc &&
{parsedDescription}
}
26 |
27 | );
28 | }
29 |
30 | export default BehindTheScenes;
31 |
--------------------------------------------------------------------------------
/server/assets/errorText.json:
--------------------------------------------------------------------------------
1 | {
2 | "api": {
3 | "certifiedDeliveryNotEnabled": "Send to CertifiedDelivery recipients is not enabled on your account. Please refer to the README to enable this setting.",
4 | "conditionalRoutingNotEnabled": "Conditional routing is not enabled on your account. Please refer to the README to enable this setting.",
5 | "documentVisibilityNotEnabled": "Document Visibility is not enabled on your account. Please refer to the README to enable this setting.",
6 | "docusignApiError": "DocuSign API error occurred",
7 | "idvNotEnabled": "IDV is not enabled on your account. Please contact Support to enable this feature.",
8 | "paymentConfigsUndefined": "Payment gateway configurations missing. Please refer to the README to set up a payment gateway and update the .env file accordingly.",
9 | "smsNotEnabled": "SMS Delivery is not enabled on your account. Please contact Support to enable this feature.",
10 | "unknownError": "An unknown error occurred. Check the server console for the stack trace."
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/server/assets/public/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
20 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=production # Set to "production" so server serves static assets
2 | USER_ID=d67e7395-867f-4ab9-b21c-3bf42ad42720 # A GUID unique to each user's DocuSign Account, located on the Apps and Keys page.
3 | API_ACCOUNT_ID=55cd2cec-297b-4f45-a6e4-e6357e3fcfd7 # A GUID unique to each user's DocuSign Account, located on the Apps and Keys page.
4 | INTEGRATION_KEY=f0b114f4-1c1b-4ca8-afdc-126ee33a0a11 # A GUID unique to each application making DocuSign API calls, located on the Apps and Keys page.
5 | DS_OAUTH_SERVER=https://account-d.docusign.com # The DocuSign authentication server, used for JWT.
6 | SESSION_SECRET=3344992992992922 # A unique string of your choice that is used to encrypt the session cookie.
7 | TARGET_ACCOUNT_ID=false
8 | REDIRECT_URI_HOME=http://localhost:8008/index # Where the user will be redirected after providing consent for JWT.
9 | REDIRECT_URI=http://localhost:8008
10 | PORT_NUMBER=8085 # server port
11 |
12 | # Payment configurations
13 | PAYMENT_GATEWAY_ACCOUNT_ID={YOUR_PAYMENT_GATEWAY_ACCOUNT_ID}
14 | PAYMENT_GATEWAY_NAME=stripe
15 | PAYMENT_GATEWAY_DISPLAY_NAME=Stripe
16 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
25 | MyGovernment Sample Application
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "proxy": "http://localhost:8085",
5 | "private": true,
6 | "dependencies": {
7 | "@hookform/error-message": "^2.0.0",
8 | "axios": "^0.25.0",
9 | "file-saver": "^2.0.5",
10 | "html-react-parser": "^1.4.10",
11 | "http-proxy-middleware": "^2.0.6",
12 | "moment": "^2.29.4",
13 | "react": "^17.0.2",
14 | "react-dom": "^17.0.2",
15 | "react-hook-form": "^7.26.1",
16 | "react-router-dom": "^6.2.1",
17 | "react-scripts": "5.0.0",
18 | "sass": "^1.49.9"
19 | },
20 | "scripts": {
21 | "start": "set port=8008 && react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/api/apiHelper.js:
--------------------------------------------------------------------------------
1 | import axios from './request';
2 |
3 | // Sends POST request using the given request and urlPath.
4 | export async function sendRequest(urlPath, request) {
5 | try {
6 | const response = await axios.post(''+urlPath, request);
7 | return handleResponse(response);
8 | } catch (error) {
9 | throw error;
10 | }
11 | }
12 |
13 | // Returns response if the status code is 200, otherwise throw error.
14 | export async function handleResponse(response) {
15 | if (response.status === 200) {
16 | // Successful response, simply return
17 | return response;
18 | }
19 |
20 | // Unknown error occurred
21 | throw new Error(
22 | 'Unknown API response error, response did not return with a status code of 200.'
23 | );
24 | }
25 |
26 | // Extracts and returns the page data to display for the given error.
27 | export function handleError(error) {
28 | const errorMessage = error.response.data;
29 | let errorPageText;
30 | if (errorMessage) {
31 | errorPageText = {
32 | title: errorMessage.title,
33 | description: errorMessage.description,
34 | };
35 | }
36 | return errorPageText;
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, useLocation } from 'react-router-dom';
3 |
4 | function Header({ text }) {
5 | const appName = 'MyGovernment';
6 | const logoUrl = '/assets/img/logo.svg';
7 | let location = useLocation();
8 |
9 | return (
10 |
11 |
34 |
35 | );
36 | }
37 |
38 | export default Header;
39 |
--------------------------------------------------------------------------------
/server/routes/templateRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | templateController,
5 | templateViewController,
6 | templateListController,
7 | templateSignersController,
8 | envelopsController,
9 | envelopDocumentsController,
10 | envelopPdfController,
11 | envelopDocumentImagesController,
12 | templateDocumentTabsController,
13 | templateEmailSendController
14 | } = require('../controllers/templateContraoller');
15 |
16 |
17 | router.post('/sendByTemplate', templateController);
18 | router.post('/sendEmailByTemplate', templateEmailSendController);
19 | router.post('/getViewByEnvelope', templateViewController);
20 | router.get('/getTemplates', templateListController);
21 | router.post('/getSigners', templateSignersController);
22 | router.post('/getEnvelopes', envelopsController);
23 | router.post('/getEnvelopePdfs', envelopPdfController);
24 | router.post('/getEnvelopeDocuments', envelopDocumentsController);
25 | router.post('/getEnvelopeDocumentImages', envelopDocumentImagesController);
26 | router.post('/getTemplateDocumentTabs', templateDocumentTabsController);
27 |
28 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal-contanier {
2 | width: 100%;
3 | height: 100vh;
4 | background-color: rgba($color: #000000, $alpha: 0.4);
5 | z-index: 500;
6 | position: fixed;
7 | top: 0;
8 | .modal-wrap {
9 | width: 600px;
10 | height: 600px;
11 | margin: 100px auto 0;
12 | background-color: #fff;
13 | overflow-y: scroll;
14 | .modal-box {
15 | padding: 24px;
16 | .header {
17 | height: 40px;
18 | border-bottom: 1px solid #ddd;
19 | .title {
20 | float: left;
21 | }
22 | .close {
23 | font-size: 18px;
24 | float: right;
25 | cursor: pointer;
26 | }
27 | }
28 | .content {
29 | padding-top: 24px;
30 | .file {
31 | height: 30px;
32 | color: #1890ff;
33 | cursor: pointer;
34 | }
35 | .imgList {
36 | display: flex;
37 | flex-wrap: wrap;
38 | .img {
39 | width: 165px;
40 | margin-right: 10px;
41 | img {
42 | width: 100%;
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_footer.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .copyright {
4 | text-align: center;
5 | }
6 |
7 | .footer-container {
8 | grid-column: 2 / 3;
9 | grid-row: 3 / 4;
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | margin-top: 30px;
14 | margin-bottom: 100px;
15 |
16 | h1 {
17 | font-size: v.$mediumTextSize + 7;
18 | }
19 | }
20 |
21 | .footer-btn {
22 | @include v.buttonStyle(v.$whiteColor, black, 210px, 40px);
23 | cursor: pointer;
24 | text-decoration: none;
25 | text-align: center;
26 | line-height: 40px;
27 | }
28 |
29 | .footer-btn-container {
30 | width: 60%;
31 | display: flex;
32 | justify-content: space-evenly;
33 | margin-top: 30px;
34 | }
35 |
36 | @media (max-width: 870px) {
37 | .footer-container {
38 | grid-column: 1 / 2;
39 | text-align: center;
40 | }
41 |
42 | .footer-btn {
43 | margin-bottom: 15px;
44 | }
45 |
46 | .footer-btn-container {
47 | width: 100%;
48 | flex-direction: column;
49 | align-items: center;
50 | }
51 | }
52 |
53 | @media (max-width: v.$mobileScreenSize) {
54 | .footer-container {
55 | width: 90%;
56 |
57 | h1 {
58 | font-size: v.$mediumTextSize;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node.js and React: MyGovernment Sample Application
2 |
3 | ## Introduction
4 |
5 | Welcome to the MyGovernment sample app! MyGovernment is written using Node.js (server) and React (client), and shows a possible integration by a government agency with DocuSign eSignature.
6 |
7 | You can find a live instance running at https://mygovernment.sampleapps.docusign.com/.
8 |
9 | DocuSign嵌入式签名示例
10 | 功能: 在docusign创建模板,选择模板生成信封,用户签名,签名增加短信验证
11 |
12 | 前端: react 后端: node.js
13 |
14 | ### 注意,本地启动访问会跨越,这里配置了跨域文件, 将client目录下的setupProxy.js文件中的域名改成你自己电脑的域名
15 | ## Running MyGovernment启动项目
16 |
17 | 前后端分别启动
18 | 前端:
19 | ```
20 | client目录
21 | npm i
22 | npm start
23 | ```
24 |
25 | 后端:
26 | ```
27 | server目录
28 | npm i
29 | npm run server
30 | ```
31 |
32 | 1. Navigate to the application folder: **`cd sample-app-mygovernment-nodejs`**
33 | 2. Navigate to the server folder: **`cd server`**
34 | 3. To start the server and client at the same time: **`npm run dev`**
35 | 4. **Or,** to run the server and client separately:
36 | - In one terminal, navigate to the server folder (**`cd server`**) and run **`npm run server`**
37 | - In a separate terminal, navigate to the client folder (**`cd client`**) and run **`npm start`**
38 | 5. Open a browser to **http://localhost:8000**
39 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_signlist.scss:
--------------------------------------------------------------------------------
1 | .signList {
2 | width: 80%;
3 | margin: 0 auto;
4 | background-color: #fff;
5 | .success {
6 | text-align: center;
7 | padding: 25px 0;
8 | }
9 | .list {
10 | .listtitle {
11 | padding-left: 24px;
12 | font-size: 16px;
13 | font-weight: bold;
14 | }
15 | .item {
16 | .label {
17 | font-weight: bold;
18 | margin-right: 40px;
19 | .text {
20 | margin-left: 6px;
21 | font-weight: normal;
22 | }
23 | }
24 | .btn {
25 | margin-right: 40px;
26 | }
27 | }
28 | .signers {
29 | .person {
30 | margin-bottom: 12px;
31 | margin-top: 10px;
32 | }
33 | .signer-table {
34 | width: 90%;
35 | border-spacing: 0;
36 | border: 1px solid #ddd;
37 | border-bottom: none;
38 | border-radius: 2px;
39 | margin-bottom: 20px;
40 | .tr {
41 | height: 35px;
42 | th, td {
43 | border-bottom: 1px solid #ddd;
44 | border-right: 1px solid #ddd;
45 | padding-left: 8px;
46 | &:last-child {
47 | border-right: none;
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_behindTheScenes.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .bts-holder {
4 | grid-column: 3 / 4;
5 | grid-row: 2 / 3;
6 | margin-left: v.$btsMarginLeft;
7 | width: v.$btsHolderWidth;
8 |
9 | h1 {
10 | font-size: v.$mediumTextSize + 3px;
11 | margin-top: 0px;
12 | transition: color 0.2s;
13 | }
14 |
15 | h1:hover {
16 | color: rgb(98, 98, 98);
17 | text-decoration: underline;
18 | }
19 | }
20 |
21 | .bts-header {
22 | cursor: pointer;
23 | display: flex;
24 | height: 30px;
25 |
26 | img {
27 | width: 19px;
28 | margin-bottom: 5px;
29 | margin-right: 8px;
30 | }
31 | }
32 |
33 | .bts-desc {
34 | height: v.$formHolderHeight + 2 * v.$formPadding - 30px;
35 | overflow-y: auto;
36 | padding-right: 20px;
37 |
38 | h2 {
39 | font-size: v.$mediumTextSize;
40 | margin-bottom: 8px;
41 | }
42 |
43 | h3 {
44 | font-size: v.$bodyTextSize + 2px;
45 | margin-bottom: 8px;
46 | }
47 |
48 | i {
49 | font-family: 'Roboto Mono', Courier, monospace;
50 | font-style: normal;
51 | // font-weight: 400;
52 | }
53 | }
54 |
55 | @media (max-width: v.$tabletScreenSize) {
56 | .bts-holder {
57 | grid-column: 1 / 2;
58 | grid-row: 3 / 4;
59 | }
60 | }
61 |
62 | @media (max-width: v.$mobileScreenSize) {
63 | .bts-holder {
64 | width: 290px;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_header.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .header {
4 | background-color: v.$whiteColor;
5 | font-size: v.$bodyTextSize;
6 | }
7 |
8 | .navbar {
9 | display: grid;
10 | grid-template-columns: v.$fourGridColumns;
11 | align-items: center;
12 | width: 100%;
13 | min-height: 60px;
14 | }
15 |
16 | .nav-logo {
17 | grid-column: 2 / 3;
18 | display: flex;
19 | align-items: center;
20 | color: black;
21 | font-weight: 700;
22 | text-decoration: none;
23 |
24 | img {
25 | width: 35px;
26 | height: auto;
27 | margin-right: 10px;
28 | }
29 | }
30 |
31 | .nav-btn {
32 | @include v.buttonStyle(v.$lightGreyColor, black, 135px, 40px);
33 | cursor: pointer;
34 | position: relative;
35 | float: right;
36 | grid-column: 3 / 4;
37 | justify-self: end;
38 | text-decoration: none;
39 | text-align: center;
40 | line-height: 40px;
41 | }
42 | .back-button {
43 | position: absolute;
44 | top: 108px;
45 | right: 18%;
46 | padding: 8px 20px;
47 | color: #fff;
48 | background-color: #1890ff;
49 | border: none;
50 | border-radius: 3px;
51 | }
52 |
53 | @media (max-width: v.$tabletScreenSize) {
54 | .navbar {
55 | grid-template-columns: 1fr;
56 | }
57 |
58 | .nav-btn {
59 | margin-right: 20px;
60 | }
61 |
62 | .nav-logo {
63 | grid-column: 1 / 2;
64 | margin-left: 20px;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_successPage.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .success-container {
4 | display: grid;
5 | grid-template-columns: 1fr;
6 | grid-template-rows: max-content 100px 1fr;
7 | justify-items: center;
8 | }
9 |
10 | .success-header-container {
11 | grid-column: 1 / 2;
12 | grid-row: 2 / 3;
13 |
14 | h1 {
15 | margin-top: 40px;
16 | }
17 | }
18 |
19 | .success-content {
20 | display: grid;
21 | grid-column: 1 / 2;
22 | grid-row: 3 / 4;
23 | justify-items: center;
24 | width: 600px;
25 | }
26 |
27 | .success-text {
28 | display: flex;
29 | margin-top: 20px;
30 | margin-bottom: 30px;
31 | line-height: 18px;
32 | }
33 |
34 | .success-check {
35 | grid-column: 1 / 2;
36 | grid-row: 1 / 2;
37 | width: 100px;
38 | height: auto;
39 | margin-top: 30px;
40 | }
41 |
42 | @media (max-width: v.$tabletScreenSize) {
43 | .success-text {
44 | display: flex;
45 | flex-direction: column;
46 | margin-top: 0px;
47 | }
48 | }
49 |
50 | @media (max-width: 950px) {
51 | .success-content {
52 | width: 410px;
53 | }
54 |
55 | .success-header-container {
56 | h1 {
57 | font-size: v.$mediumTextSize + 7px;
58 | }
59 | }
60 | }
61 |
62 | @media (max-width: 680px) {
63 | .success-header-container {
64 | text-align: center;
65 | width: 90%;
66 | }
67 |
68 | .success-text {
69 | margin-top: 10px;
70 |
71 | .user-profile {
72 | padding-bottom: 0px;
73 | }
74 | }
75 |
76 | .success-content {
77 | width: 90%;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/client/src/components/RequireAuth.js:
--------------------------------------------------------------------------------
1 | import axios from '../api/request';
2 | import React, { useEffect, useRef, useState } from 'react';
3 | import { Navigate, Outlet, useNavigate } from 'react-router-dom';
4 | import { handleError } from '../api/apiHelper';
5 |
6 | function RequireAuth() {
7 | let navigate = useNavigate();
8 | let mountedRef = useRef(true);
9 | const [authed, setAuthed] = useState(undefined);
10 |
11 | useEffect(() => {
12 | isLoggedIn();
13 |
14 | // Clean up
15 | return () => {
16 | mountedRef.current = false;
17 | };
18 | }, []);
19 |
20 | async function isLoggedIn() {
21 | try {
22 | let response = await axios.get('/auth/isLoggedIn');
23 | // Only set states if the component is mounted, otherwise return null.
24 | if (!mountedRef.current) return null;
25 | setAuthed(response.data);
26 | } catch (error) {
27 | console.log(error);
28 | const errorPageText = handleError(error);
29 | navigate('/error', { state: errorPageText });
30 | }
31 | }
32 |
33 | // If the user has not logged in yet, then they are redirected
34 | // to the login screen.
35 | return (
36 | <>
37 | {authed === undefined ? (
38 | // Empty section to make sure that the footer copyright text stays in the
39 | // same spot while static assets are being loaded in.
40 |
41 | ) : authed ? (
42 |
43 | ) : (
44 |
45 | )}
46 | >
47 | );
48 | }
49 |
50 | export default RequireAuth;
51 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_popup.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | $padding: 25px;
4 |
5 | .popup {
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100%;
11 | background-color: rgba(0, 0, 0, 0.75);
12 |
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | }
17 |
18 | .box {
19 | display: grid;
20 | grid-template-rows: 40px 1fr;
21 |
22 | position: relative;
23 | width: 45%;
24 | margin: 0 auto;
25 | height: auto;
26 | max-height: 70vh;
27 | background: v.$whiteColor;
28 | border-radius: v.$borderRadius;
29 | line-height: 20px;
30 | }
31 |
32 | .popup-header {
33 | grid-row: 1 / 2;
34 | background-color: black;
35 | border-top-left-radius: v.$borderRadius;
36 | border-top-right-radius: v.$borderRadius;
37 |
38 | h1 {
39 | color: v.$whiteColor;
40 | font-size: v.$mediumTextSize;
41 | padding-left: $padding;
42 | margin-top: 10px;
43 | margin-bottom: 10px;
44 | }
45 | }
46 |
47 | .popup-desc {
48 | grid-row: 2 / 3;
49 | overflow-y: auto;
50 | padding-top: 10px;
51 | padding-left: $padding;
52 | padding-right: $padding;
53 | padding-bottom: $padding;
54 |
55 | h2 {
56 | margin-top: 22px;
57 | margin-bottom: 5px;
58 | }
59 |
60 | h3 {
61 | margin-bottom: 4px;
62 | }
63 | }
64 |
65 | @media (max-width: v.$tabletScreenSize) {
66 | .box {
67 | width: 60%;
68 | }
69 | }
70 |
71 | @media (max-width: 800px) {
72 | .box {
73 | width: 80%;
74 | }
75 | }
76 |
77 | .close-button {
78 | cursor: pointer;
79 | font-size: v.$bigTextSize;
80 | font-weight: 400;
81 | color: v.$whiteColor;
82 | position: absolute;
83 | top: 8px;
84 | right: 15px;
85 | }
86 |
--------------------------------------------------------------------------------
/private.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEogIBAAKCAQEAiWVhtbgrgtA8/Mw1DASd5DiJaeZ5PGPTo6isOFar229LHroz
3 | 0Vba9xR1jOB2URIU43j0e3JA7+uQYgLoDHsqgkugoFqmNRfNqisfz09GpqGlgtYX
4 | ewLAkUY7OlxL2tGw2xDBwbWl3/OrDmOCzOJTHcYRuhgTXc3jNw7qbbr7pKZnhCSF
5 | iFT26C1MOruRP/z8WWYjr1fhj66MiODxxEMmdHGiHA4ik77fjVg/Mx+sPQ2UkewK
6 | G8oFjFA5uQ2uOtBJnTErCok3rxNKe6Ibzxfgxd14ag23rnhCUSNPmFm7sf79odxc
7 | MljNtvAJGAtwKkhPcZtWOdZB0Ho63tPtCQs+8wIDAQABAoIBAAT52HuR03WV4BeR
8 | t7wbMLKyv3tH3w0qWmBbe/1AWlIEqlZPDRBgUmbPZWB4QqC2BG6mk0gjP6nQwLZj
9 | /45wKX51ibg1AD79ATRQAoNqDhd71Dg0U75fP6UjQ4LeE9s1a+/LIBFJaFv/garw
10 | PKz552O1KDAyrgTgL4HvdtsJghKY7KIrr3shFU3sx01N96nUK8HHEQPzAw5IftE2
11 | 3OyflQP6JTg1BZequ2Chl7s52PUn5qTPm//6eFUybdZd8ZlFRuLweNpDEdZoTJ/p
12 | kjvnOKj4FUYxw4bNRGsR9MBwv7Eu61RHFqNYQBUwYQQyzHQq4sNYH+naFFpP/RL+
13 | YWsSbTkCgYEAu47P7PCZCl5HJ1XNT0MlRD2qBYRcKARurmELO+sVv8mNml4vZi+A
14 | 1OfdRjUIlWDpPButbyIxDl1lOD1dH7RFoJvCIHZ6S5+hifRrC+6dTtfEVRftaGA0
15 | gWi7gBnwpQV+F/z6PCIduY2YqSUvHYxOH7XsMReMKipjYXGB/b2E5kkCgYEAu4iT
16 | WlMvA+OS2oR6GT1OMQWybtf74tJSdl+9Uc/Y02GxJ3e8kRD+x0ALI52oMR912oPq
17 | R/gXO7VxLUub2g6CcjDMah1sXrcl7LoaTVvDzGwag43EcfmQlA5+KACKZvsscXXP
18 | 9dgoKFZgQDrt8ObaNtOx0E51Np2V6vSvrCimS1sCgYBKrLCSSLTWEPVJbvsAXN6A
19 | jgCck7dRY3phyVd8sruFEY1kca1zhORJYEuUQVc6ikwyV161Car4aiz7uErUbaTv
20 | LjSbUjCc3WCmmX7pUEandna/3nfyf6NIBtfoR+us+EPc7yb+PCMTlpG6foiEvjD4
21 | pSj13sc6nofU6ylzSjqYqQKBgA+bSBgGQC1krP3+dGLRVUaDINxUoSO5OR+czBaY
22 | 44SD5shQEKNJ9MoELGkkX7Dm21n6DG882EIh0W9hkXDOMFDseraCCFjBiShwPGwf
23 | rOXAQVydWbHagQuxQRJ3KQ107bfrhAkDmiPxPEVcIh0gORzC9VNOlDadWrKY9l2v
24 | bFdtAoGAM1Ek+qapMAZnLYT8zO+COv+e88Hc9C30bC5+4+K3ffeAXVyT5NyhBl4i
25 | WYAhJV+czEjn0QK8baH578uGhzSaTBFAzzRYnaGpLw0J7IocB8HrCAO0o6CFyXOZ
26 | UnODa7edR18vb2QxMJ5oX5ICIsOzgING2CzgrGHX+IMKO3FiUUM=
27 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------
/client/src/pages/SmallBusinessLoan/SubmittedLoan.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { handleError } from '../../api/apiHelper';
4 | import axios from '../../api/request';
5 | import Success from '../Success';
6 |
7 | function SubmittedLoan({ text }) {
8 | let mountedRef = useRef(true);
9 | const sageAvatarUrl = '/assets/img/sage_avatar.png';
10 | const blaireAvatarUrl = '/assets/img/blaire_avatar.png';
11 | const [lenderName, setLenderName] = useState('');
12 | const [avatarUrl, setAvatarUrl] = useState('');
13 | let navigate = useNavigate();
14 |
15 | useEffect(() => {
16 | getLoanAmount();
17 |
18 | // Clean up
19 | return () => {
20 | mountedRef.current = false;
21 | };
22 | });
23 |
24 | // GETs the loan amount that the user inputted in their loan application,
25 | // and sets the lender name accordingly.
26 | async function getLoanAmount() {
27 | try {
28 | let response = await axios.get('/loanApplication/submitted');
29 |
30 | // Only set states if the component is mounted, otherwise return null.
31 | if (!mountedRef.current) return null;
32 |
33 | setLenderName(response.data);
34 |
35 | if (response.data === text.names.smallLenderName) {
36 | setAvatarUrl(sageAvatarUrl);
37 | } else {
38 | setAvatarUrl(blaireAvatarUrl);
39 | }
40 | } catch (error) {
41 | console.log(error);
42 | const errorPageText = handleError(error);
43 | // navigate('/error', { state: errorPageText });
44 | }
45 | }
46 |
47 | return (
48 |
54 | );
55 | }
56 |
57 | export default SubmittedLoan;
58 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_pages.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .content-section {
4 | min-height: 88vh;
5 | }
6 |
7 | .container {
8 | display: grid;
9 | grid-template-columns: v.$fourGridColumns;
10 | grid-template-rows: 110px 1fr;
11 | }
12 |
13 | .header-container {
14 | grid-column: 2 / 4;
15 | grid-row: 1 / 2;
16 | }
17 |
18 | h1 {
19 | font-size: v.$bigTextSize;
20 | margin-top: 50px;
21 | }
22 |
23 | .button-container {
24 | display: flex;
25 | width: 100%;
26 | justify-content: space-between;
27 | }
28 |
29 | .black-button {
30 | @include v.buttonStyle(black, white, 45%);
31 | }
32 |
33 | .black-button:hover {
34 | @include v.buttonHoverStyle(#2b2b2b);
35 | }
36 |
37 | .black-button:disabled {
38 | @include v.buttonStyle(v.$greyColor, white, 45%);
39 | cursor: default;
40 | }
41 |
42 | .grey-button {
43 | @include v.buttonStyle(v.$lightGreyColor, black, 45%);
44 | }
45 |
46 | .grey-button:hover {
47 | @include v.buttonHoverStyle(#e6e6e6);
48 | }
49 |
50 | .grey-button-fixed-size {
51 | @include v.buttonStyle(v.$lightGreyColor, black, 200px);
52 | margin-top: 30px;
53 | }
54 |
55 | .grey-button-fixed-size:hover {
56 | @include v.buttonHoverStyle(#e6e6e6);
57 | }
58 |
59 | .error-container {
60 | display: flex;
61 | flex-direction: column;
62 | align-items: center;
63 | text-align: center;
64 | }
65 |
66 | @media (max-width: v.$tabletScreenSize) {
67 | .container {
68 | grid-template-columns: 1fr;
69 | grid-template-rows: 100px 1fr;
70 | justify-items: center;
71 | }
72 |
73 | .header-container {
74 | grid-column: 1 / 2;
75 | grid-row: 1 / 2;
76 | }
77 |
78 | h1 {
79 | margin-top: 40px;
80 | }
81 | }
82 |
83 | @media (max-width: v.$mobileScreenSize) {
84 | .container {
85 | grid-template-rows: 70px 1fr;
86 | }
87 |
88 | h1 {
89 | font-size: v.$bigTextSize - 10;
90 | margin-top: 30px;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/client/src/pages/SmallBusinessLoan/SmallBusinessLoan.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { handleError, sendRequest } from '../../api/apiHelper';
4 | import Form from '../../components/Form';
5 | import BehindTheScenes from '../../components/BehindTheScenes';
6 |
7 | function SmallBusinessLoan({ text, formText, btsText, userFlowText }) {
8 | let navigate = useNavigate();
9 | const [requesting, setRequesting] = useState(false);
10 | const avatarUrl = '/assets/img/default_avatar.png';
11 |
12 | // Sends POST request to server requesting redirect URL for embedded signing
13 | // based on the info the user put in the form.
14 | async function handleSubmit(event) {
15 | setRequesting(true);
16 |
17 | // Make request body
18 | const body = {
19 | signerName: event.signerName,
20 | signerEmail: event.signerEmail,
21 | contractAmount: event.contractAmount,
22 | };
23 |
24 | // Send request to server
25 | try {
26 | const response = await sendRequest('/loanApplication', body);
27 | // Received URL for embedded signing, redirect user
28 | if (response.status === 200) {
29 | window.location = response.data;
30 | }
31 | } catch (error) {
32 | const errorPageText = handleError(error);
33 | navigate('/error', { state: errorPageText });
34 | }
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 |
{text.title}
42 |
43 |
52 |
56 |
57 |
58 | );
59 | }
60 |
61 | export default SmallBusinessLoan;
62 |
--------------------------------------------------------------------------------
/client/src/pages/TrafficTicket/TrafficTicket.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { handleError, sendRequest } from '../../api/apiHelper';
4 | import Form from '../../components/Form';
5 | import BehindTheScenes from '../../components/BehindTheScenes';
6 |
7 | function TrafficTicket({ text, formText, btsText, userFlowText }) {
8 | let navigate = useNavigate();
9 | const [requesting, setRequesting] = useState(false);
10 | const avatarUrl = '/assets/img/default_avatar.png';
11 |
12 | // Sends POST request to server requesting redirect URL for embedded signing
13 | // based on the info the user put in the form.
14 | async function handleSubmit(event) {
15 | setRequesting(true);
16 |
17 | // Make request body
18 | const body = {
19 | signerName: event.signerName,
20 | signerEmail: event.signerEmail,
21 | countryCode: event.countryCode,
22 | phoneNumber: event.phoneNumber,
23 | };
24 |
25 | // Send request to server
26 | try {
27 | const response = await sendRequest('/trafficTicket', body);
28 |
29 | // Received URL for embedded signing, redirect user
30 | if (response.status === 200) {
31 | window.location = response.data;
32 | }
33 | } catch (error) {
34 | console.log(error);
35 | const errorPageText = handleError(error);
36 | navigate('/error', { state: errorPageText });
37 | }
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
{text.title}
45 |
46 |
55 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default TrafficTicket;
65 |
--------------------------------------------------------------------------------
/client/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import { sendRequest } from '../api/apiHelper';
3 |
4 | const Modal = (props) => {
5 | const [fileList, setFileList] = useState([])
6 | const [imgList, setImgList] = useState([])
7 | const {visible, envelopeId, handleCancel} = props
8 |
9 | // 获取文件列表
10 | const getList = async () => {
11 | try {
12 | const {data} = await sendRequest('/template/getEnvelopeDocuments', {envelopeId});
13 | setFileList(data)
14 |
15 | } catch (error) {
16 | console.log(error);
17 | }
18 | }
19 |
20 | useEffect(() => {
21 | if (visible) {
22 | getList()
23 | }
24 | }, [visible])
25 |
26 | const onCancel = () => {
27 | handleCancel()
28 | setFileList([])
29 | setImgList([])
30 | }
31 |
32 | // 查看文件图片
33 | const viewImage = async(recored) => {
34 |
35 | try {
36 | let pages = recored.pages ? recored.pages : [{sequence: '1'}]
37 | const arr = []
38 | for (const item of pages) {
39 | const { data } = await sendRequest('/template/getEnvelopeDocumentImages',{
40 | envelopeId,
41 | documentId: recored.documentId,
42 | pageNumber: item.sequence,
43 | })
44 | arr.push(`data:image/png;base64, ${data}`)
45 | }
46 | setImgList(arr)
47 |
48 | } catch (error) {
49 | console.log(error);
50 | }
51 | }
52 |
53 | return (
54 | visible ?
55 |
56 |
57 |
61 |
62 | {fileList.length && fileList.map(item =>
63 |
viewImage(item)}>{item.name}
64 | )}
65 |
66 | {imgList.map(item =>
67 |
68 |

69 |
70 | )}
71 |
72 |
73 |
74 |
75 |
: <>>
76 | )
77 | }
78 |
79 | export default Modal
--------------------------------------------------------------------------------
/client/src/pages/Success.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import parse from 'html-react-parser';
3 | import { useNavigate } from 'react-router-dom';
4 | import UserProfile from '../components/UserProfile';
5 |
6 | function Success({
7 | text,
8 | title,
9 | description,
10 | includeContinueButton,
11 | continueUrl,
12 | avatarUrl,
13 | userRoleName,
14 | }) {
15 | let navigate = useNavigate();
16 | const backLinkName = 'Back to Home';
17 | const continueLinkName = 'Continue';
18 | const checkMarkURL = '/assets/img/check.svg';
19 |
20 | return (
21 |
22 |
23 | {avatarUrl === undefined && userRoleName === undefined && (
24 |

30 | )}
31 |
32 | {text ?
{text.title}
: {title}
}
33 |
34 |
35 |
36 |
37 | {avatarUrl && userRoleName && (
38 |
43 | )}
44 | {text ? text.description : parse(description)}
45 |
46 |
47 | {includeContinueButton && continueUrl ? (
48 |
49 |
57 |
58 |
64 |
65 | ) : (
66 |
69 | )}
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default Success;
77 |
--------------------------------------------------------------------------------
/client/src/assets/scss/variables/_variables.scss:
--------------------------------------------------------------------------------
1 | // Colors
2 | $bgColor: #f6f6f6;
3 | $greyColor: #787878;
4 | $lightGreyColor: #eeeeee;
5 | $redColor: #cc0f0f;
6 | $whiteColor: white;
7 |
8 | // Drop shadows
9 | $formDropShadow: 0px 5px 20px rgba(0, 0, 0, 0.06);
10 | $buttonDropShadow: 3px 3px 3px rgba(0, 0, 0, 0.11);
11 |
12 | // Text sizes
13 | $bodyTextSize: 14px;
14 | $mediumTextSize: 18px;
15 | $bigTextSize: 34px;
16 |
17 | // Form sizes
18 | $formHolderWidth: 460px;
19 | $formHolderHeight: 545px;
20 | $formPadding: 35px;
21 |
22 | // Behind the scenes sizes
23 | $btsHolderWidth: 450px;
24 | $btsMarginLeft: 40px;
25 |
26 | // Card sizes
27 | $cardWidthLarge: 275px;
28 | $cardWidthMedium: 210px;
29 |
30 | // Other sizes
31 | $borderRadius: 2px;
32 |
33 | // Formatting
34 | $fourGridColumns: 1fr ($formHolderWidth + 2 * $formPadding)
35 | ($btsHolderWidth + $btsMarginLeft) 1fr;
36 | $threeGridColumns: 1fr
37 | ($formHolderWidth + 2 * $formPadding + $btsHolderWidth + $btsMarginLeft) 1fr;
38 |
39 | // Responsive screen sizes
40 | $mobileScreenSize: 580px;
41 | $tabletScreenSize: 1100px;
42 |
43 | // Button
44 | @mixin buttonStyle(
45 | $bgColor,
46 | $textColor,
47 | $width,
48 | $height: 45px,
49 | $includeShadow: true
50 | ) {
51 | width: $width;
52 | height: $height;
53 | font-family: Raleway, sans-serif;
54 | font-weight: 600;
55 | color: $textColor;
56 | background-color: $bgColor;
57 | border: none;
58 | border-radius: $borderRadius;
59 | @if ($includeShadow) {
60 | box-shadow: $buttonDropShadow;
61 | }
62 | transition: background-color 0.4s;
63 | }
64 |
65 | @mixin buttonHoverStyle($bgColor) {
66 | background-color: $bgColor;
67 | cursor: pointer;
68 | }
69 |
70 | @mixin card($borderColor) {
71 | width: $cardWidthLarge;
72 | height: 420px;
73 | padding: 20px;
74 | background-color: $whiteColor;
75 | box-shadow: $formDropShadow;
76 | display: flex;
77 | flex-direction: column;
78 | justify-content: flex-end;
79 | border-bottom: 5px solid $borderColor;
80 | border-radius: $borderRadius;
81 | margin-bottom: 30px;
82 |
83 | ul {
84 | margin-top: 5px;
85 | font-size: $bodyTextSize - 1px;
86 | line-height: 18px;
87 | padding-left: 25px;
88 | }
89 | }
90 |
91 | @mixin cardHoverStyle {
92 | content: '';
93 | position: absolute;
94 | top: 100%;
95 | width: 100%;
96 | left: 0;
97 | height: 3px;
98 | border-radius: 2px;
99 | }
100 |
--------------------------------------------------------------------------------
/client/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import axios from '../api/request';
2 | import React, { useEffect } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 | import { handleError } from '../api/apiHelper';
5 | import Footer from '../components/Footer';
6 | import Card from '../components/Card';
7 |
8 | function Home({ text, footerText }) {
9 | let navigate = useNavigate();
10 |
11 | useEffect(() => {
12 | getUserInfo();
13 | }, []);
14 |
15 | // If the previous screen was the login screen, then
16 | // make sure the server has the necessary user information
17 | // stored for making DocuSign API calls.
18 | async function getUserInfo() {
19 | try {
20 | let response = await axios.get('/auth/login');
21 |
22 | // If the user revoked consent after logging in, check to make
23 | // sure they still have consent
24 | if (response.status === 210) {
25 | console.log('Consent URL: ' + response.data);
26 | window.location = response.data;
27 | }
28 | } catch (error) {
29 | console.log(error);
30 | const errorPageText = handleError(error);
31 | navigate('/error', { state: errorPageText });
32 | }
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
{text.title}
40 |
41 |
42 |
50 |
58 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | export default Home;
75 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_login.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .login-container {
4 | display: grid;
5 | grid-template-columns: v.$fourGridColumns;
6 | grid-template-rows: min-content 1fr min-content;
7 | }
8 |
9 | .login-header-container {
10 | grid-column: 2 / 3;
11 | grid-row: 1 / 2;
12 | }
13 |
14 | .login-desc {
15 | grid-column: 2 / 3;
16 | grid-row: 2 / 3;
17 | width: 90%;
18 | line-height: 20px;
19 | }
20 |
21 | .login-btn-container {
22 | display: flex;
23 | justify-content: space-between;
24 | margin-top: 50px;
25 | }
26 |
27 | .more-info-btn-container {
28 | display: flex;
29 | flex-direction: column;
30 | align-items: center;
31 | }
32 |
33 | .more-info-btn {
34 | cursor: pointer;
35 | color: black;
36 | font-family: Raleway, sans-serif;
37 | font-weight: 500;
38 | background: none;
39 | border: none;
40 | text-decoration: underline;
41 | margin-top: 10px;
42 | width: max-content;
43 | }
44 |
45 | .more-info-btn:hover {
46 | color: v.$greyColor;
47 | }
48 |
49 | .login-button {
50 | @include v.buttonStyle(black, white, 200px);
51 | }
52 |
53 | .login-button:hover {
54 | @include v.buttonHoverStyle(#2b2b2b);
55 | }
56 |
57 | .login-button:disabled {
58 | @include v.buttonStyle(v.$greyColor, white, 200px);
59 | cursor: default;
60 | }
61 |
62 | .github-button {
63 | @include v.buttonStyle(v.$lightGreyColor, black, 200px);
64 | cursor: pointer;
65 | text-decoration: none;
66 | text-align: center;
67 | line-height: 45px;
68 | }
69 |
70 | .github-button:hover {
71 | @include v.buttonHoverStyle(#e6e6e6);
72 | }
73 |
74 | .hero {
75 | grid-column: 3 / 4;
76 | grid-row: 1 / 3;
77 | width: 100%;
78 | height: auto;
79 | margin-top: 40px;
80 | }
81 |
82 | @media (max-width: v.$tabletScreenSize) {
83 | .hero {
84 | grid-column: 1 / 2;
85 | grid-row: 3 / 4;
86 | width: 60%;
87 | height: auto;
88 | }
89 |
90 | .login-container {
91 | grid-template-columns: 1fr;
92 | justify-items: center;
93 | }
94 |
95 | .login-header-container {
96 | grid-column: 1 / 2;
97 | text-align: center;
98 | width: 90%;
99 | }
100 |
101 | .login-btn-container {
102 | justify-content: space-evenly;
103 | }
104 |
105 | .login-desc {
106 | grid-column: 1 / 2;
107 | width: 50%;
108 | }
109 | }
110 |
111 | @media (max-width: 890px) {
112 | .login-desc {
113 | width: 70%;
114 | }
115 |
116 | .hero {
117 | width: 80%;
118 | }
119 | }
120 |
121 | @media (max-width: 670px) {
122 | .login-button {
123 | width: 150px;
124 | }
125 |
126 | .github-button {
127 | width: 150px;
128 | }
129 | }
130 |
131 | @media (max-width: v.$mobileScreenSize) {
132 | .login-button {
133 | width: 110px;
134 | }
135 |
136 | .github-button {
137 | width: 110px;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/pages/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from '../api/request';
3 | import { useNavigate } from 'react-router-dom';
4 | import { handleError } from '../api/apiHelper';
5 | import Popup from '../components/Popup';
6 |
7 | function Login({ text, githubText, btsText }) {
8 | const [submitted, setSubmitted] = useState(false);
9 | const [isOpen, setIsOpen] = useState(false);
10 | const heroUrl = '/assets/img/hero.png';
11 | let navigate = useNavigate();
12 |
13 | // Logs the user in, and redirects the user to a consent window if
14 | // they haven't provided consent yet.
15 | async function handleLogin() {
16 | try {
17 | setSubmitted(true);
18 |
19 | let response = await axios.get('/auth/login');
20 | // If user has never logged in before, redirect to consent screen
21 | if (response.status === 210) {
22 | console.log('Consent URL: ' + response.data);
23 | window.location = response.data;
24 | } else if (response.status === 200) {
25 | // User is logged in, redirect to home page
26 | navigate('/index');
27 | }
28 | } catch (error) {
29 | console.log(error);
30 | const errorPageText = handleError(error);
31 | navigate('/error', { state: errorPageText });
32 | }
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
{text.title}
40 |
41 |
42 | {text.description}
43 |
44 |
71 |
72 |

73 |
74 | {isOpen && (
75 |
{
78 | setIsOpen(!isOpen);
79 | }}
80 | />
81 | )}
82 |
83 |
84 | );
85 | }
86 |
87 | export default Login;
88 |
--------------------------------------------------------------------------------
/client/src/pages/TrafficTicket/WitnessStatement.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { handleError, sendRequest } from '../../api/apiHelper';
4 | import Form from '../../components/Form';
5 | import BehindTheScenes from '../../components/BehindTheScenes';
6 | import Success from '../Success';
7 |
8 | function WitnessStatement({ text, formText, btsText }) {
9 | let navigate = useNavigate();
10 | const [requesting, setRequesting] = useState(false);
11 | const [submitted, setSubmitted] = useState(false);
12 | const codyAvatarUrl = '/assets/img/cody_avatar.png';
13 | const paulaAvatarUrl = '/assets/img/paula_avatar.png';
14 | const policeName = text.names.policeName;
15 | const description = text.submitted.contestedSent.description.replaceAll(
16 | '{policeName}',
17 | policeName
18 | );
19 |
20 | // Sends POST request to server with phone number to sends
21 | // an SMS delivery to the given phone, then updates the title
22 | // and description of the page.
23 | async function handleSubmit(event) {
24 | setRequesting(true);
25 |
26 | // Make request body
27 | const body = {
28 | signerName: policeName,
29 | countryCode: event.countryCode,
30 | phoneNumber: event.phoneNumber,
31 | };
32 |
33 | // Send request to server
34 | try {
35 | const response = await sendRequest('/trafficTicket/sms', body);
36 | console.log(response.data);
37 |
38 | // Set submitted to true to rerender page.
39 | setSubmitted(true);
40 | } catch (error) {
41 | console.log(error);
42 | const errorPageText = handleError(error);
43 | navigate('/error', { state: errorPageText });
44 | }
45 | }
46 |
47 | return (
48 | <>
49 | {!submitted ? (
50 |
51 |
52 |
53 |
{text.submitted.contestedForm.title}
54 |
55 |
66 |
70 |
71 |
72 | ) : (
73 |
79 | )}
80 | >
81 | );
82 | }
83 |
84 | export default WitnessStatement;
85 |
--------------------------------------------------------------------------------
/server/docusign/workflow.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Util functions that help with determining user account settings.
3 | */
4 | const eSignSdk = require('docusign-esign');
5 |
6 | /**
7 | * Returns the workflow ID for IDV.
8 | */
9 | const getIdvWorkflowId = async (args) => {
10 | // Make AccountApi client to call
11 | let eSignApi = new eSignSdk.ApiClient();
12 | eSignApi.setBasePath(args.basePath);
13 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
14 | let accountsApi = new eSignSdk.AccountsApi(eSignApi);
15 |
16 | let workflowId = null;
17 | let workflowResults = await accountsApi.getAccountIdentityVerification(
18 | args.accountId
19 | );
20 |
21 | // Find the workflow ID corresponding to the name "DocuSign ID Verification"
22 | workflowResults.identityVerification.forEach((workflow) => {
23 | if (workflow.defaultName === 'Phone Authentication') {
24 | workflowId = workflow.workflowId;
25 | }
26 | });
27 |
28 | return workflowId;
29 | };
30 |
31 | /**
32 | * Returns whether or not the user has send to CertifiedDelivery recipients
33 | * enabled on their account.
34 | */
35 | const hasCertifiedDeliveryEnabled = async (args) => {
36 | const res = await hasSettingEnabled(args, 'sendToCertifiedDeliveryEnabled');
37 | return res;
38 | };
39 |
40 | /**
41 | * Returns whether or not the user has conditional routing enabled on their
42 | * account.
43 | */
44 | const hasConditionalRoutingEnabled = async (args) => {
45 | const res = await hasSettingEnabled(
46 | args,
47 | 'allowAdvancedRecipientRoutingConditional'
48 | );
49 | return res;
50 | };
51 |
52 | /**
53 | * Returns whether or not the user has Document Visibility
54 | * enabled on their account.
55 | */
56 | const hasDocumentVisibilityEnabled = async (args) => {
57 | const res = await hasSettingEnabled(args, 'allowDocumentVisibility');
58 | return res;
59 | };
60 |
61 | /**
62 | * Returns whether or not the user has SMS delivery enabled on
63 | * their account.
64 | */
65 | const hasSmsEnabled = async (args) => {
66 | const res = await hasSettingEnabled(args, 'allowSMSDelivery');
67 | return res;
68 | };
69 |
70 | /**
71 | * Helper function that returns the result of whether or not the given setting
72 | * is enabled on the user's account.
73 | */
74 | const hasSettingEnabled = async (args, setting) => {
75 | // Make AccountApi client to call
76 | let eSignApi = new eSignSdk.ApiClient();
77 | eSignApi.setBasePath(args.basePath);
78 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
79 | let accountsApi = new eSignSdk.AccountsApi(eSignApi);
80 |
81 | let workflowResults = await accountsApi.listSettings(args.accountId);
82 |
83 | if (workflowResults.hasOwnProperty(setting)) {
84 | return workflowResults[setting];
85 | } else {
86 | throw new Error(`Given setting \"${setting}\" does not exist.`);
87 | }
88 | };
89 |
90 | module.exports = {
91 | getIdvWorkflowId,
92 | hasCertifiedDeliveryEnabled,
93 | hasConditionalRoutingEnabled,
94 | hasDocumentVisibilityEnabled,
95 | hasSmsEnabled,
96 | };
97 |
--------------------------------------------------------------------------------
/server/controllers/passportController.js:
--------------------------------------------------------------------------------
1 | const { checkToken } = require('./jwtController');
2 | const {
3 | sendEnvelopeFromTemplate,
4 | } = require('../docusign/useTemplate');
5 | const { getRecipientViewUrl } = require('../docusign/envelope');
6 | const { getIdvWorkflowId } = require('../docusign/workflow');
7 | const errorText = require('../assets/errorText.json').api;
8 | const AppError = require('../utils/appError');
9 |
10 | // Set constants
11 | const signerClientId = '1001';
12 | const dsReturnUrl =
13 | process.env.REDIRECT_URI + '/apply-for-passport/passport-sign';
14 | const dsPingUrl = process.env.REDIRECT_URI + '/';
15 |
16 | /**
17 | * Controller that creates and sends an envelope to the signer.
18 | */
19 | const createController = async (req, res, next) => {
20 | // Check the access token, which will also update the token
21 | // if it is expired
22 | await checkToken(req);
23 |
24 | // Construct arguments
25 | const { body } = req;
26 | const envelopeArgs = {
27 | signerEmail: body.signerEmail,
28 | signerName: body.signerName,
29 | countryCode: body.countryCode,
30 | phoneNumber: body.phoneNumber,
31 | templateId: body.templateId,
32 | status: 'sent',
33 |
34 | signerClientId: signerClientId,
35 | dsReturnUrl: dsReturnUrl,
36 | dsPingUrl: dsPingUrl,
37 | };
38 | const args = {
39 | accessToken: req.session.accessToken,
40 | basePath: req.session.basePath,
41 | accountId: req.session.accountId,
42 | envelopeArgs: envelopeArgs,
43 | };
44 |
45 | let results = null;
46 |
47 | // Send the envelope to signer
48 | try {
49 | // Verify that the user has Payment related environment variables set up,
50 | // and if they don't send error message back.
51 | // if (
52 | // !process.env.PAYMENT_GATEWAY_ACCOUNT_ID ||
53 | // !process.env.PAYMENT_GATEWAY_NAME ||
54 | // !process.env.PAYMENT_GATEWAY_DISPLAY_NAME
55 | // ) {
56 | // throw new AppError(403, errorText.paymentConfigsUndefined);
57 | // }
58 |
59 | // Step 1 start
60 | // Get the workflowId and add it to envelopeArgs
61 | const workflowId = await getIdvWorkflowId(args);
62 | // console.log(6666, workflowId)
63 | // If IDV is not enabled in the account, send back error message.
64 | if (workflowId === null) {
65 | throw new AppError(403, errorText.idvNotEnabled);
66 | }
67 |
68 | args.envelopeArgs.workflowId = workflowId;
69 | // Step 1 end
70 |
71 | // Step 2 start
72 | // Send the envelope and get the envelope ID
73 | const envelopeId = await sendEnvelopeFromTemplate(args.envelopeArgs);
74 | // Step 2 end
75 |
76 | // Set results. We don't need the envelopeId for the rest of this example,
77 | // but you can store it for use later in other use cases.
78 | // results = { envelopeId: envelopeId };
79 |
80 | // Set results
81 | results = { envelopeId: envelopeId };
82 | } catch (error) {
83 | console.log('Error sending the envelope.');
84 | next(error);
85 | }
86 |
87 | if (results) {
88 | // res.status(200).send('Envelope successfully sent!');
89 | req.session.loanAppEnvelopeId = results.envelopeId;
90 |
91 | // Send back redirect URL for embedded signing
92 | res.status(200).send(results);
93 | }
94 | };
95 |
96 | module.exports = {
97 | createController,
98 | };
99 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_home.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .home-container {
4 | display: grid;
5 | grid-template-columns: v.$threeGridColumns;
6 | grid-template-rows: 110px 1fr min-content;
7 | }
8 |
9 | .home-header-container {
10 | grid-column: 2 / 3;
11 | grid-row: 1 / 2;
12 | }
13 |
14 | .card-holder {
15 | grid-column: 2 / 3;
16 | grid-row: 2 / 3;
17 | display: flex;
18 | justify-content: space-between;
19 | }
20 |
21 | .small-business-card {
22 | @include v.card(#579ecd);
23 | }
24 |
25 | .passport-card {
26 | @include v.card(#d7ad4a);
27 | }
28 |
29 | .traffic-ticket-card {
30 | @include v.card(#a44c52);
31 | }
32 |
33 | .image-holder {
34 | display: flex;
35 | flex-direction: column;
36 | width: 100%;
37 | align-items: center;
38 | margin-bottom: 60px;
39 | }
40 |
41 | .card-btn-container {
42 | display: flex;
43 | justify-content: center;
44 | margin-top: 20px;
45 | margin-bottom: 0px;
46 | }
47 |
48 | .small-business-btn {
49 | @include v.buttonStyle(#ccd6dc, black, v.$cardWidthLarge, 55px, false);
50 | font-size: v.$bodyTextSize;
51 | }
52 |
53 | .small-business-btn:hover {
54 | @include v.buttonHoverStyle(#b8c7d0);
55 | }
56 |
57 | .traffic-ticket-btn {
58 | @include v.buttonStyle(#e1d0d2, black, v.$cardWidthLarge, 55px, false);
59 | font-size: v.$bodyTextSize;
60 | }
61 |
62 | .traffic-ticket-btn:hover {
63 | @include v.buttonHoverStyle(#d5bdc0);
64 | }
65 |
66 | .passport-btn {
67 | @include v.buttonStyle(#efe8d7, black, v.$cardWidthLarge, 55px, false);
68 | font-size: v.$bodyTextSize;
69 | }
70 |
71 | .passport-btn:hover {
72 | @include v.buttonHoverStyle(#e7ddc4);
73 | }
74 |
75 | .card-title {
76 | font-size: v.$mediumTextSize + 3px;
77 | }
78 |
79 | .card-features {
80 | font-size: v.$bodyTextSize;
81 | margin-top: 0px;
82 | margin-bottom: 0px;
83 | }
84 |
85 | @media (max-width: v.$tabletScreenSize) {
86 | .home-header-container {
87 | grid-column: 2 / 3;
88 | grid-row: 1 / 2;
89 | width: 800px;
90 | }
91 |
92 | .home-container {
93 | grid-template-columns: 1fr max-content 1fr;
94 | grid-template-rows: min-content 1fr;
95 | }
96 |
97 | .card-title {
98 | font-size: v.$mediumTextSize + 1px;
99 | }
100 |
101 | .small-business-card {
102 | width: v.$cardWidthMedium;
103 | height: 380px;
104 | }
105 |
106 | .passport-card {
107 | width: v.$cardWidthMedium;
108 | height: 380px;
109 | }
110 |
111 | .traffic-ticket-card {
112 | width: v.$cardWidthMedium;
113 | height: 380px;
114 | }
115 |
116 | .small-business-btn {
117 | width: v.$cardWidthMedium;
118 | }
119 |
120 | .traffic-ticket-btn {
121 | width: v.$cardWidthMedium;
122 | }
123 |
124 | .passport-btn {
125 | width: v.$cardWidthMedium;
126 | }
127 | }
128 |
129 | @media (max-width: 870px) {
130 | .home-header-container {
131 | display: grid;
132 | grid-column: 1 / 2;
133 | grid-row: 1 / 2;
134 | width: 100%;
135 | justify-items: center;
136 | }
137 |
138 | .home-container {
139 | grid-template-columns: 1fr;
140 | align-items: center;
141 | justify-items: center;
142 | }
143 |
144 | .card-holder {
145 | display: grid;
146 | grid-column: 1 / 2;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ path: `${process.env.PWD}/.env` });
2 | const path = require('path');
3 | const express = require('express');
4 | const cookieParser = require('cookie-parser');
5 | const cookieSession = require('cookie-session');
6 | const bodyParser = require('body-parser');
7 | const helmet = require('helmet');
8 | const errorText = require('./assets/errorText.json').api;
9 | const AppError = require('./utils/appError');
10 |
11 | // Route imports
12 | const authRouter = require('./routes/jwtRouter');
13 | const passportRouter = require('./routes/passportRouter');
14 | const loanRouter = require('./routes/loanRouter');
15 | const trafficRouter = require('./routes/trafficRouter');
16 | const templateRouter = require('./routes/templateRouter');
17 |
18 | // Max session age
19 | const maxSessionAge = 1000 * 60 * 60 * 24 * 1; // One day
20 |
21 | // Configure server
22 | const app = express()
23 | .use(helmet())
24 | .use(bodyParser.json())
25 | .use(cookieParser())
26 | .use(
27 | cookieSession({
28 | name: 'MyGovernmentApp',
29 | maxAge: maxSessionAge,
30 | secret: process.env.SESSION_SECRET,
31 | httpOnly: true,
32 | secure: false, // Set to false when testing on localhost, otherwise to "true"
33 | sameSite: 'lax',
34 | })
35 | );
36 |
37 | app.get('/', (req, res) => {
38 | res.send('Server started');
39 | res.end();
40 | });
41 |
42 | // Routing
43 | app.use('/auth', authRouter);
44 | app.use('/passportApplication', passportRouter);
45 | app.use('/loanApplication', loanRouter);
46 | app.use('/trafficTicket', trafficRouter);
47 | app.use('/template', templateRouter);
48 |
49 | app.all('*', function(req, res, next) {
50 | res.header("Access-Control-Allow-Origin", "*");
51 | res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
52 | res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
53 | next();
54 | });
55 |
56 | // Error handler
57 | app.use((err, req, res, next) => {
58 | if (err instanceof AppError) {
59 | // AppError will contain a specific status code and custom message
60 | const statusCode = err.statusCode || 500;
61 | const errorMessage = err.message;
62 |
63 | res.status(statusCode).send({
64 | title: errorText.docusignApiError,
65 | description: `Status code: ${statusCode}
${errorMessage}`,
66 | });
67 | } else if (err && err.response && err.response.body) {
68 | // DocuSign API specific error, extract error code and message
69 | const statusCode = 500;
70 | const errorBody = err && err.response && err.response.body;
71 | const errorCode = errorBody && errorBody.errorCode;
72 | const errorMessage = errorBody && errorBody.message;
73 |
74 | res.status(statusCode).send({
75 | title: errorText.docusignApiError,
76 | description: `Status code: ${statusCode}
${errorCode}: ${errorMessage}`,
77 | });
78 | } else {
79 | console.log('Unknown error occurred.');
80 | console.log(err);
81 |
82 | res.status(500).send({
83 | title: errorText.docusignApiError,
84 | description: `Status code: ${statusCode}
${errorText.unknownError}`,
85 | });
86 | }
87 | });
88 |
89 | // Serve static assets if in production
90 | if (process.env.NODE_ENV === 'production') {
91 | console.log('In production');
92 | app.use('/assets', express.static(path.join(__dirname, 'assets', 'public')));
93 | }
94 |
95 | const port = process.env.PORT_NUMBER;
96 | app.listen(port, () => {
97 | console.log(`Server started and listening on port ${port}`);
98 | });
99 |
--------------------------------------------------------------------------------
/client/src/pages/TrafficTicket/SubmittedTrafficTicket.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { handleError } from '../../api/apiHelper';
3 | import axios from '../../api/request';
4 | import Success from '../Success';
5 | import { useNavigate } from 'react-router-dom';
6 |
7 | function SubmittedTrafficTicket({ text }) {
8 | let navigate = useNavigate();
9 | let mountedRef = useRef(true);
10 | const millieAvatarUrl = '/assets/img/millie_avatar.png';
11 | const codyAvatarUrl = '/assets/img/cody_avatar.png';
12 | const witnessStatementUrl =
13 | '/receive-traffic-ticket/request-witness-statement';
14 | const mitigationClerkName = text.names.mitigationClerkName;
15 | const contestedClerkName = text.names.contestedClerkName;
16 | const [userChoice, setUserChoice] = useState('');
17 | const [userRoleName, setUserRoleName] = useState('');
18 | const [title, setTitle] = useState('');
19 | const [description, setDescription] = useState('');
20 | const [avatarUrl, setAvatarUrl] = useState('');
21 |
22 | useEffect(() => {
23 | getUserChoice();
24 |
25 | // Clean up
26 | return () => {
27 | mountedRef.current = false;
28 | };
29 | });
30 |
31 | // GETs the user choice (plead guilty, no contest, request mitigation
32 | // hearing, or request contested hearing). Then sets the title and
33 | // description of the page accordingly.
34 | async function getUserChoice() {
35 | try {
36 | let response = await axios.get('/trafficTicket/submitted');
37 |
38 | // Only set states if the component is mounted, otherwise return null.
39 | if (!mountedRef.current) return null;
40 |
41 | setUserChoice(response.data);
42 |
43 | // Set the avatar URL, title, and description based on user choice
44 | if (response.data === 'Mitigation') {
45 | setTitle(text.submitted.mitigation.title);
46 | setDescription(
47 | text.submitted.mitigation.description.replaceAll(
48 | '{mitigationClerkName}',
49 | mitigationClerkName
50 | )
51 | );
52 | setUserRoleName(mitigationClerkName);
53 | setAvatarUrl(millieAvatarUrl);
54 | } else if (response.data === 'Contested') {
55 | setTitle(text.submitted.contestedSuccess.title);
56 | setDescription(
57 | text.submitted.contestedSuccess.description.replaceAll(
58 | '{contestedClerkName}',
59 | contestedClerkName
60 | )
61 | );
62 | setUserRoleName(contestedClerkName);
63 | setAvatarUrl(codyAvatarUrl);
64 | } else {
65 | setTitle(text.submitted.paidFine.title);
66 | setDescription(text.submitted.paidFine.description);
67 | }
68 | } catch (error) {
69 | console.log(error);
70 | const errorPageText = handleError(error);
71 | navigate('/error', { state: errorPageText });
72 | }
73 | }
74 |
75 | return (
76 | <>
77 | {userChoice === 'Contested' ? (
78 |
86 | ) : userChoice === 'Mitigation' ? (
87 |
93 | ) : (
94 |
95 | )}
96 | >
97 | );
98 | }
99 |
100 | export default SubmittedTrafficTicket;
101 |
--------------------------------------------------------------------------------
/client/src/assets/scss/components/_form.scss:
--------------------------------------------------------------------------------
1 | @use '../variables/variables' as v;
2 |
3 | .form-holder {
4 | grid-column: 2 / 3;
5 | grid-row: 2 / 3;
6 | width: v.$formHolderWidth;
7 | height: v.$formHolderHeight;
8 | background-color: v.$whiteColor;
9 | padding: v.$formPadding;
10 | margin-bottom: 30px;
11 | border-radius: v.$borderRadius;
12 | box-shadow: v.$formDropShadow;
13 |
14 | label {
15 | font-size: 14px;
16 | flex-basis: 40%;
17 | }
18 |
19 | label:after {
20 | content: '*';
21 | padding-left: 8px;
22 | font-size: 17px;
23 | color: v.$redColor;
24 | line-height: v.$bodyTextSize;
25 | font-family: sans-serif;
26 | }
27 |
28 | input {
29 | width: 100%;
30 | box-sizing: border-box;
31 | padding: 7px;
32 | font-family: Raleway, sans-serif;
33 | font-weight: 500;
34 | margin-bottom: 30px;
35 | }
36 | }
37 | .template-holder {
38 | grid-column: 2 / 3;
39 | grid-row: 2 / 3;
40 | width: 800px;
41 | height: 480px;
42 | background-color: v.$whiteColor;
43 | padding: v.$formPadding;
44 | margin-bottom: 30px;
45 | border-radius: v.$borderRadius;
46 | box-shadow: v.$formDropShadow;
47 | label {
48 | font-size: 14px;
49 | flex-basis: 40%;
50 | }
51 | .signer {
52 | label {
53 | flex-basis: 80%;
54 | margin-top: 25px;
55 | }
56 | }
57 |
58 | label:after {
59 | content: '*';
60 | padding-left: 8px;
61 | font-size: 17px;
62 | color: v.$redColor;
63 | line-height: v.$bodyTextSize;
64 | font-family: sans-serif;
65 | }
66 |
67 | select {
68 | width: 50%;
69 | box-sizing: border-box;
70 | padding: 7px;
71 | font-family: Raleway, sans-serif;
72 | font-weight: 500;
73 | margin-bottom: 30px;
74 | }
75 | .singer-list {
76 | border: 1px solid #ddd;
77 | border-radius: 2px;
78 | margin-bottom: 40px;
79 | .signer-table {
80 | width: 100%;
81 | border-spacing: 0;
82 | .theader {
83 | height: 35px;
84 | th {
85 | border-bottom: 1px solid #ddd;
86 | border-right: 1px solid #ddd;
87 | &:last-child {
88 | border-right: none;
89 | }
90 | }
91 | }
92 | .nodata {
93 | height: 40px;
94 | color: #999;
95 | text-align: center;
96 | }
97 | }
98 | input {
99 | width: 100%;
100 | box-sizing: border-box;
101 | padding: 7px;
102 | font-family: Raleway, sans-serif;
103 | font-weight: 500;
104 | }
105 | }
106 | }
107 |
108 | @media (max-width: v.$tabletScreenSize) {
109 | .form-holder {
110 | grid-column: 1 / 2;
111 | grid-row: 2 / 3;
112 | }
113 | }
114 |
115 | @media (max-width: v.$mobileScreenSize) {
116 | .form-holder {
117 | width: 290px;
118 | padding: 20px;
119 | }
120 | }
121 |
122 | .form-text-container {
123 | display: flex;
124 | flex-wrap: wrap;
125 | padding-bottom: 5px;
126 | }
127 |
128 | .error {
129 | font-size: 14px;
130 | color: v.$redColor;
131 | flex-basis: 60%;
132 | text-align: right;
133 | }
134 |
135 | .user-profile {
136 | display: flex;
137 | flex-direction: column;
138 | align-items: center;
139 | justify-content: center;
140 | width: 100%;
141 | padding-bottom: 30px;
142 |
143 | h3 {
144 | font-size: v.$bodyTextSize;
145 | font-weight: 700;
146 | margin-top: 15px;
147 | margin-bottom: 5px;
148 | }
149 |
150 | button {
151 | cursor: pointer;
152 | font-size: v.$bodyTextSize - 2;
153 | color: v.$greyColor;
154 | background-color: v.$whiteColor;
155 | border: none;
156 | text-decoration: underline;
157 | }
158 |
159 | img {
160 | width: 80px;
161 | height: 80px;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/server/docusign/templates.js:
--------------------------------------------------------------------------------
1 | const docusign = require('docusign-esign');
2 |
3 | // 获取模板列表
4 | const getTemplateList = async (args) => {
5 | let eSignApi = new docusign.ApiClient();
6 | eSignApi.setBasePath(args.basePath);
7 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
8 | let templatesApi = new docusign.TemplatesApi(eSignApi)
9 | let results = null;
10 |
11 | results = await templatesApi.listTemplates(args.accountId);
12 |
13 | let templateList = results.envelopeTemplates;
14 | console.log(`templatelist get success`);
15 |
16 | return templateList;
17 | }
18 |
19 |
20 | // 获取模板签约人列表
21 | const getTemplatSigners = async (args) => {
22 | let eSignApi = new docusign.ApiClient();
23 | eSignApi.setBasePath(args.basePath);
24 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
25 | let templatesApi = new docusign.TemplatesApi(eSignApi)
26 |
27 | const results = await templatesApi.listRecipients(args.accountId, args.templateId);
28 |
29 | let templateSigners = results.signers;
30 | console.log(`templateInfo get success`);
31 |
32 | return templateSigners;
33 | }
34 |
35 | // 获取信封列表
36 | const getenvelops = async (args) => {
37 | let eSignApi = new docusign.ApiClient();
38 | eSignApi.setBasePath(args.basePath);
39 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
40 | let envelopesApi = new docusign.EnvelopesApi(eSignApi)
41 |
42 | const results = await envelopesApi.listStatusChanges(
43 | args.accountId,
44 | {
45 | folderIds: args.folderIds,
46 | fromDate: args.fromDate,
47 | include: 'recipients'
48 | }
49 | );
50 |
51 | let envelopes = results.envelopes;
52 | console.log(`envelopes get success`);
53 |
54 | return envelopes;
55 | }
56 |
57 | // 获取信封文件pdf
58 | const getenvelopPdf = async (args) => {
59 | let eSignApi = new docusign.ApiClient();
60 | eSignApi.setBasePath(args.basePath);
61 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
62 | let envelopesApi = new docusign.EnvelopesApi(eSignApi)
63 |
64 | const results = await envelopesApi.getDocument(
65 | args.accountId,
66 | args.envelopeId,
67 | 'combined'
68 | );
69 |
70 | console.log(`envelopepdfs get success`);
71 |
72 | return { mimetype: 'application/pdf', docName: 'signer.pdf', fileBytes: results };
73 | }
74 |
75 | // 获取信封文件列表
76 | const getenvelopDocuments = async (args) => {
77 | let eSignApi = new docusign.ApiClient();
78 | eSignApi.setBasePath(args.basePath);
79 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
80 | let envelopesApi = new docusign.EnvelopesApi(eSignApi)
81 |
82 | const results = await envelopesApi.listDocuments(
83 | args.accountId,
84 | args.envelopeId,
85 | );
86 |
87 | console.log(`envelopeDocuments get success`);
88 |
89 | return results.envelopeDocuments;
90 | }
91 | // 根据文档ID获取文档图片
92 | const getenvelopDocumentImages = async (args) => {
93 | let eSignApi = new docusign.ApiClient();
94 | eSignApi.setBasePath(args.basePath);
95 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
96 | let envelopesApi = new docusign.EnvelopesApi(eSignApi)
97 |
98 | const results = await envelopesApi.getDocumentPageImage(
99 | args.accountId,
100 | args.envelopeId,
101 | args.documentId,
102 | args.pageNumber,
103 | );
104 |
105 | console.log(`pages get success`);
106 |
107 | return Buffer.from(results).toString('base64');
108 | }
109 | // 根据文档ID获取文档tab
110 | const getTemplateDocumentTabs = async (args) => {
111 | let eSignApi = new docusign.ApiClient();
112 | eSignApi.setBasePath(args.basePath);
113 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
114 | let envelopesApi = new docusign.TemplatesApi(eSignApi)
115 |
116 | const results = await envelopesApi.getDocumentTabs(
117 | args.accountId,
118 | args.templateId,
119 | // args.envelopeId,
120 | args.documentId,
121 | );
122 |
123 | console.log(`documentTab get success`);
124 |
125 | return results;
126 | }
127 |
128 | module.exports = {
129 | getTemplateList,
130 | getTemplatSigners,
131 | getenvelops,
132 | getenvelopDocuments,
133 | getenvelopPdf,
134 | getenvelopDocumentImages,
135 | getTemplateDocumentTabs
136 | };
--------------------------------------------------------------------------------
/server/docusign/envelopes/makeSmsEnvelope.js:
--------------------------------------------------------------------------------
1 | const eSignSdk = require('docusign-esign');
2 | const fs = require('fs');
3 | const text = require('../../assets/public/text.json').trafficTicket.envelope;
4 |
5 | /**
6 | * Creates and returns an SMS delivery envelope definition
7 | * for the police officer.
8 | */
9 | function makeSmsEnvelope(args) {
10 | /////////////// Create document for envelope ///////////////
11 | // Read and create document from file in the local directory
12 | let docPdfBytes = fs.readFileSync(args.docFile);
13 | let docb64 = Buffer.from(docPdfBytes).toString('base64');
14 | let doc = new eSignSdk.Document.constructFromObject({
15 | documentBase64: docb64,
16 | name: text.smsDocName, // can be different from actual file name
17 | fileExtension: 'pdf',
18 | documentId: '1',
19 | });
20 |
21 | /////////////// Create signHere tab ///////////////
22 | let signHere = eSignSdk.SignHere.constructFromObject({
23 | recipientId: '1',
24 | documentId: '1',
25 | pageNumber: '1',
26 | tabLabel: 'signHere',
27 | xPosition: '123',
28 | yPosition: '612',
29 | });
30 |
31 | /////////////// Create fullName tab ///////////////
32 | let fullName = eSignSdk.FullName.constructFromObject({
33 | recipientId: '1',
34 | documentId: '1',
35 | pageNumber: '1',
36 | xPosition: '155',
37 | yPosition: '120',
38 | required: 'true',
39 | tabLabel: 'fullName',
40 | height: '12',
41 | width: '60',
42 | });
43 |
44 | /////////////// Create dateSigned tab ///////////////
45 | let dateSigned1 = eSignSdk.DateSigned.constructFromObject({
46 | recipientId: '1',
47 | documentId: '1',
48 | pageNumber: '1',
49 | tabLabel: 'dateSigned1',
50 | xPosition: '306',
51 | yPosition: '94',
52 | });
53 |
54 | let dateSigned2 = eSignSdk.DateSigned.constructFromObject({
55 | recipientId: '1',
56 | documentId: '1',
57 | pageNumber: '1',
58 | tabLabel: 'dateSigned2',
59 | xPosition: '151',
60 | yPosition: '196',
61 | });
62 |
63 | let dateSigned3 = eSignSdk.DateSigned.constructFromObject({
64 | recipientId: '1',
65 | documentId: '1',
66 | pageNumber: '1',
67 | tabLabel: 'dateSigned3',
68 | xPosition: '93',
69 | yPosition: '674',
70 | });
71 |
72 | /////////////// Create attachment tab ///////////////
73 | let attachmentTab = eSignSdk.SignerAttachment.constructFromObject({
74 | recipientId: '1',
75 | documentId: '1',
76 | pageNumber: '1',
77 | xPosition: '65',
78 | yPosition: '466',
79 | optional: 'true',
80 | });
81 |
82 | /////////////// Create text fields ///////////////
83 | let time = eSignSdk.Text.constructFromObject({
84 | recipientId: '1',
85 | documentId: '1',
86 | pageNumber: '1',
87 | xPosition: '94',
88 | yPosition: '222',
89 | required: 'false',
90 | tabLabel: 'time',
91 | height: '12',
92 | width: '70',
93 | });
94 |
95 | let location = eSignSdk.Text.constructFromObject({
96 | recipientId: '1',
97 | documentId: '1',
98 | pageNumber: '1',
99 | xPosition: '64',
100 | yPosition: '266',
101 | required: 'false',
102 | tabLabel: 'location',
103 | height: '12',
104 | width: '250',
105 | });
106 |
107 | let details = eSignSdk.Text.constructFromObject({
108 | recipientId: '1',
109 | documentId: '1',
110 | pageNumber: '1',
111 | shared: 'false',
112 | value: text.smsDescription,
113 | originalValue: text.smsDescription,
114 | required: 'true',
115 | locked: 'false',
116 | concealValueOnDocument: 'false',
117 | disableAutoSize: 'false',
118 | maxLength: '4000',
119 | tabLabel: 'details',
120 | font: 'lucidaconsole',
121 | fontColor: 'black',
122 | fontSize: 'size9',
123 | xPosition: '60',
124 | yPosition: '314',
125 | width: '550',
126 | height: '131',
127 | tabType: 'text',
128 | });
129 |
130 | /////////////// Create the phone recipient of the envelope ///////////////
131 | // Create a signer recipient to sign the document with the tabs and phone number
132 | let signer = eSignSdk.Signer.constructFromObject({
133 | name: args.signerName,
134 | recipientId: '1',
135 | phoneNumber: eSignSdk.RecipientPhoneNumber.constructFromObject({
136 | countryCode: args.countryCode,
137 | number: args.phoneNumber,
138 | }),
139 | tabs: eSignSdk.Tabs.constructFromObject({
140 | dateSignedTabs: [dateSigned1, dateSigned2, dateSigned3],
141 | fullNameTabs: [fullName],
142 | signerAttachmentTabs: [attachmentTab],
143 | signHereTabs: [signHere],
144 | textTabs: [details, location, time],
145 | }),
146 | });
147 |
148 | // Add the recipients to the envelope object
149 | let recipients = eSignSdk.Recipients.constructFromObject({
150 | signers: [signer],
151 | });
152 |
153 | // Request that the envelope be sent by setting |status| to "sent".
154 | // To request that the envelope be created as a draft, set to "created"
155 | return eSignSdk.EnvelopeDefinition.constructFromObject({
156 | emailSubject: text.smsEmailSubject,
157 | documents: [doc],
158 | status: args.status,
159 | recipients: recipients,
160 | });
161 | }
162 |
163 | module.exports = {
164 | makeSmsEnvelope,
165 | };
166 |
--------------------------------------------------------------------------------
/client/src/pages/PassPort/PassportSign.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import { handleError, sendRequest } from '../../api/apiHelper';
3 | import axios from '../../api/request';
4 | import { useNavigate } from 'react-router-dom';
5 | import moment from 'moment'
6 | import FileSaver from 'file-saver'
7 | import Modal from '../../components/Modal'
8 |
9 | const PassportSign = () => {
10 | const [signList, setSignList] = useState([])
11 | const [modalVisible, setModalVisible] = useState(false)
12 | const [envelopeId, setEnvelopId] = useState('')
13 | const dateTime = 'YYYY-MM-DD HH:mm:ss'
14 | let navigate = useNavigate();
15 |
16 | // 获取已签约列表
17 | const getList = async () => {
18 |
19 | const body = {
20 | folderIds: 'completed, waiting_for_others, awaiting_my_signature', // 信封状态: 已完成,等待其他人签署,等待我签署
21 | fromDate: moment().subtract(30, 'days').format(), // 日期范围
22 | }
23 | try {
24 | const {data} = await sendRequest('/template/getEnvelopes', body);
25 | setSignList(data)
26 |
27 | } catch (error) {
28 | console.log(error);
29 | }
30 | }
31 |
32 | useEffect(() => {
33 | getList()
34 | }, [])
35 |
36 | // 生成签约视图
37 | const toSign = async (item) => {
38 | // Make request body
39 | const body = {
40 | ...item,
41 | };
42 |
43 | try {
44 | const response = await sendRequest('/template/getViewByEnvelope', body);
45 | if (response.status === 200) {
46 | window.location = response.data;
47 | }
48 | } catch (error) {
49 | console.log(error);
50 | const errorPageText = handleError(error);
51 | navigate('/error', { state: errorPageText });
52 | }
53 | }
54 |
55 | // 下载合同
56 | const toDownload = async (item) => {
57 | try {
58 | const {data} = await axios({
59 | url: '/template/getEnvelopePdfs',
60 | method: 'post',
61 | responseType: 'blob',
62 | data: {envelopeId: item.envelopeId}
63 | });
64 | console.log(data)
65 | if (data && data.size) {
66 | FileSaver.saveAs(
67 | data,
68 | item.emailSubject
69 | )
70 | }
71 | } catch (error) {
72 | console.log(error);
73 | const errorPageText = handleError(error);
74 | navigate('/error', { state: errorPageText });
75 | }
76 | }
77 |
78 | // 查看文件列表
79 | const toView = async (envelopeId) => {
80 | setEnvelopId(envelopeId)
81 | setModalVisible(true)
82 | }
83 |
84 | const handleCancel = () => {
85 | setModalVisible(false)
86 | }
87 |
88 | return (
89 |
90 |
91 |
签约成功
92 |
98 |
152 |
153 |
154 |
155 |
156 | )
157 | }
158 |
159 | export default PassportSign
--------------------------------------------------------------------------------
/server/docusign/envelopByTemplate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | * Example 009: Send envelope using a template
4 | * @author DocuSign
5 | */
6 |
7 | const docusign = require("docusign-esign");
8 |
9 | /**
10 | * This function does the work of creating the envelope
11 | * @param {object} args object
12 | */
13 | const sendEnvelopeFromTemplate = async (args) => {
14 | // Data for this method
15 | // args.basePath
16 | // args.accessToken
17 | // args.accountId
18 |
19 | let dsApiClient = new docusign.ApiClient();
20 | dsApiClient.setBasePath(args.basePath);
21 | dsApiClient.addDefaultHeader("Authorization", "Bearer " + args.accessToken);
22 | let envelopesApi = new docusign.EnvelopesApi(dsApiClient);
23 |
24 | // await getTemplatDocument(args)
25 | // Step 1. Make the envelope request body
26 | let envelope = await makeEnvelope(args.envelopeArgs);
27 |
28 | // Step 2. call Envelopes::create API method
29 | // Exceptions will be caught by the calling function
30 | let results = await envelopesApi.createEnvelope(args.accountId, {
31 | envelopeDefinition: envelope,
32 | });
33 | let envelopeId = results.envelopeId;
34 | console.log(`Envelope was created. EnvelopeId ${envelopeId}`);
35 | return envelopeId;
36 | };
37 |
38 | /**
39 | * Creates envelope from the template
40 | * @function
41 | * @param {Object} args object
42 | * @returns {Envelope} An envelope definition
43 | * @private
44 | */
45 | async function makeEnvelope(args) {
46 |
47 | // create the envelope definition
48 | let env = new docusign.EnvelopeDefinition();
49 | let signers = [{
50 | email: args.signerEmail,
51 | name: args.signerName,
52 | roleName: 'partyA',
53 | recipientId: '1',
54 | clientUserId: args.signerClientId,
55 | emailNotification: {
56 | supportedLanguage: 'zh_CN'
57 | },
58 | identityVerification: {
59 | workflowId: args.workflowId,
60 | steps: null,
61 | "inputOptions":
62 | [{
63 | "name":"phone_number_list",
64 | "valueType":"PhoneNumberList",
65 | "phoneNumberList":[
66 | {
67 | "countryCode":args.countryCode,
68 | "code":"1",
69 | "number":args.phoneNumber
70 | }
71 | ]
72 | }],
73 | "idCheckConfigurationName":""
74 | },
75 | }]
76 |
77 | const compTemplate = docusign.CompositeTemplate.constructFromObject({
78 | compositeTemplateId: "1",
79 | serverTemplates: [
80 | docusign.ServerTemplate.constructFromObject({
81 | sequence: "1",
82 | templateId: args.templateId,
83 | }),
84 | ],
85 | // Add the roles via an inlineTemplate
86 | inlineTemplates: [
87 | docusign.InlineTemplate.constructFromObject({
88 | sequence: "2",
89 | recipients: {
90 | signers,
91 | },
92 | }),
93 | ],
94 | });
95 |
96 | env.compositeTemplates = [compTemplate]
97 | env.status = "sent"; // We want the envelope to be sent
98 |
99 | return env;
100 | }
101 |
102 | /**
103 | * This function does the work of creating the envelope
104 | * @param {object} args object
105 | */
106 | const sendEmailByEnvelopeId = async (args) => {
107 | // Data for this method
108 | // args.basePath
109 | // args.accessToken
110 | // args.accountId
111 |
112 | let dsApiClient = new docusign.ApiClient();
113 | dsApiClient.setBasePath(args.basePath);
114 | dsApiClient.addDefaultHeader("Authorization", "Bearer " + args.accessToken);
115 | let envelopesApi = new docusign.EnvelopesApi(dsApiClient);
116 |
117 | // await getTemplatDocument(args)
118 | // Step 1. Make the envelope request body
119 | let recipients = await makeEamilEnvelope(args);
120 |
121 | // Step 2. call Envelopes::create API method
122 | // Exceptions will be caught by the calling function
123 | let results = await envelopesApi.createRecipient(args.accountId, args.envelopeId, {
124 | recipients,
125 | resend_envelope: 'true'
126 | });
127 | let envelopeId = results.envelopeId;
128 | console.log(`Envelope was created. EnvelopeId ${envelopeId}`);
129 | return envelopeId;
130 | };
131 |
132 | /**
133 | * Creates envelope from the template
134 | * @function
135 | * @param {Object} args object
136 | * @returns {Envelope} An envelope definition
137 | * @private
138 | */
139 | async function makeEamilEnvelope(args) {
140 |
141 | // create the envelope definition
142 |
143 | let signers = [{
144 | email: args.email,
145 | name: args.name,
146 | roleName: args.roleName,
147 | recipientId: '1',
148 | // clientUserId: args.signerClientId,
149 | emailNotification: {
150 | supportedLanguage: 'zh_CN'
151 | },
152 | // identityVerification: {
153 | // workflowId: args.workflowId,
154 | // steps: null,
155 | // "inputOptions":
156 | // [{
157 | // "name":"phone_number_list",
158 | // "valueType":"PhoneNumberList",
159 | // "phoneNumberList":[
160 | // {
161 | // "countryCode":args.countryCode,
162 | // "code":"1",
163 | // "number":args.phoneNumber
164 | // }
165 | // ]
166 | // }],
167 | // "idCheckConfigurationName":""
168 | // },
169 | }]
170 |
171 | let recipients = docusign.Recipients.constructFromObject({
172 | signers,
173 | });
174 | return recipients
175 | }
176 |
177 | module.exports = { sendEnvelopeFromTemplate, sendEmailByEnvelopeId };
178 |
--------------------------------------------------------------------------------
/server/docusign/envelope.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Util functions that help with envelope creation and sending.
3 | */
4 | const eSignSdk = require('docusign-esign');
5 |
6 | /**
7 | * Creates and sends the envelope, then returns the envelope ID
8 | * corresponding to the sent envelope.
9 | */
10 | const sendEnvelope = async (envelopeDefinition, args) => {
11 | // Create API client to call
12 | let eSignApi = new eSignSdk.ApiClient();
13 | eSignApi.setBasePath(args.basePath);
14 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
15 | let envelopesApi = new eSignSdk.EnvelopesApi(eSignApi);
16 | let results = null;
17 |
18 | // Call Envelopes::create API method
19 | // Exceptions will be caught by the calling function
20 | results = await envelopesApi.createEnvelope(args.accountId, {
21 | envelopeDefinition: envelopeDefinition,
22 | });
23 |
24 | let envelopeId = results.envelopeId;
25 |
26 | // 官方例子实际使用有问题,版本问题?
27 | // let prefillTabs = eSignSdk.PrefillTabs.constructFromObject({
28 | // 'textTabs':[{
29 | // recipientId: '1',
30 | // documentId: '1',
31 | // pageNumber: '1',
32 | // xPosition: '145',
33 | // yPosition: '292',
34 | // required: 'true',
35 | // tabLabel: '合同金额',
36 | // height: '20',
37 | // width: '220',
38 | // bold: true,
39 | // // value: args.envelopeArgs.contractAmount,
40 | // value: 60000,
41 | // }]});
42 |
43 | // let tabs = new eSignSdk.Tabs();
44 | // tabs.prefillTabs = prefillTabs;
45 | // envelopesApi.createDocumentTabs(args.accountId, envelopeId, '1', tabs);
46 |
47 | console.log(`Envelope was created. EnvelopeId ${envelopeId}`);
48 | return envelopeId;
49 | };
50 |
51 | /**
52 | * Creates recipient view request for embedded signing, and returns the redirect URL
53 | * for embedded signing.
54 | */
55 | const getRecipientViewUrl = async (envelopeId, args) => {
56 | // Create API client to call
57 | console.log(444, args)
58 | let eSignApi = new eSignSdk.ApiClient();
59 | eSignApi.setBasePath(args.basePath);
60 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
61 | let envelopesApi = new eSignSdk.EnvelopesApi(eSignApi);
62 |
63 | // Create the recipient view request object
64 | const viewRequest = new eSignSdk.RecipientViewRequest.constructFromObject({
65 | authenticationMethod: 'none',
66 | clientUserId: args.envelopeArgs.signerClientId,
67 | // clientUserId: '80f7e864-c2f7-4878-9f84-7eaacd6d8855',
68 | recipientId: '1',
69 | returnUrl: args.envelopeArgs.dsReturnUrl,
70 | userName: args.envelopeArgs.signerName,
71 | email: args.envelopeArgs.signerEmail,
72 | pingFrequency: '600',
73 | pingUrl: args.envelopeArgs.dsPingUrl, // optional setting
74 | });
75 |
76 | // Call the CreateRecipientView API
77 | // Exceptions will be caught by the calling function
78 | let recipientView = await envelopesApi.createRecipientView(
79 | args.accountId,
80 | envelopeId,
81 | {
82 | recipientViewRequest: viewRequest,
83 | }
84 | );
85 | console.log(recipientView.url)
86 | return recipientView.url;
87 | };
88 |
89 | /**
90 | * This function does the work of getting the specified tab value
91 | * for the given envelope.
92 | */
93 | const getEnvelopeTabData = async (args) => {
94 | // Create API client to call
95 | let eSignApi = new eSignSdk.ApiClient();
96 | eSignApi.setBasePath(args.basePath);
97 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
98 | let envelopesApi = new eSignSdk.EnvelopesApi(eSignApi);
99 |
100 | let results = null;
101 |
102 | // Call EnvelopeFormData::get
103 | // Exceptions will be caught by the calling function
104 | results = await envelopesApi.getFormData(args.accountId, args.envelopeId);
105 |
106 | // Get the requested tab value
107 | let tabValue;
108 | results.recipientFormData.forEach((recipient) => {
109 | // Find the first recipient whose name matches the signer name
110 | if (recipient.name === args.signerName) {
111 | // Find the first tab with given tab name to get the value the user chose
112 | recipient.formData.forEach((tab) => {
113 | if (tab.name === args.tabName) {
114 | tabValue = tab.value;
115 | }
116 | });
117 | }
118 | });
119 |
120 | return tabValue;
121 | };
122 |
123 | /**
124 | * This function does the work of creating the brand if it doesn't already exist,
125 | * and returns the brandId.
126 | */
127 | const createBrand = async (args) => {
128 | // Construct your API headers
129 | let eSignApi = new eSignSdk.ApiClient();
130 | eSignApi.setBasePath(args.basePath);
131 | eSignApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
132 | let accountsApi = new eSignSdk.AccountsApi(eSignApi);
133 | let results = null;
134 | let brandId;
135 |
136 | // Check to see if the brand already exists in the user's account and set
137 | // the brandId if found
138 | results = await accountsApi.listBrands(args.accountId);
139 | results.brands && results.brands.forEach((brand) => {
140 | if (brand.brandName === args.envelopeArgs.brandName) {
141 | brandId = brand.brandId;
142 | }
143 | });
144 |
145 | // Brand not found, create new brand
146 | if (!brandId) {
147 | // Construct the request body
148 | let callback = {
149 | brand: {
150 | brandName: args.envelopeArgs.brandName,
151 | defaultBrandLanguage: args.envelopeArgs.defaultBrandLanguage,
152 | colors: args.envelopeArgs.colors,
153 | },
154 | };
155 | // Call the eSignature REST API to create the brand
156 | results = await accountsApi.createBrand(args.accountId, callback);
157 |
158 | // The results contains only one brand object with info about the
159 | // brand we just created, so get the corresponding brandId
160 | brandId = results.brands[0].brandId;
161 | }
162 |
163 | return brandId;
164 | };
165 |
166 | module.exports = {
167 | sendEnvelope,
168 | getRecipientViewUrl,
169 | getEnvelopeTabData,
170 | createBrand,
171 | };
172 |
--------------------------------------------------------------------------------
/server/controllers/loanController.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { checkToken } = require('./jwtController');
3 | const text = require('../assets/public/text.json').smallBusinessLoan;
4 | const {
5 | makeLoanApplicationEnvelope,
6 | } = require('../docusign/envelopes/makeLoanApplication');
7 | const {
8 | getRecipientViewUrl,
9 | sendEnvelope,
10 | getEnvelopeTabData,
11 | createBrand,
12 | } = require('../docusign/envelope');
13 | const {
14 | hasConditionalRoutingEnabled,
15 | hasDocumentVisibilityEnabled,
16 | } = require('../docusign/workflow');
17 | const errorText = require('../assets/errorText.json').api;
18 | const AppError = require('../utils/appError');
19 |
20 | // Set constants
21 | const signerClientId = '1000'; // The id of the signer within this application.
22 | const docsPath = path.resolve(__dirname, '../docusign/pdf');
23 | const docFile = 'sign1.pdf';
24 | const dsReturnUrl =
25 | process.env.REDIRECT_URI + '/apply-for-small-business-loan/submitted-loan';
26 | // const dsReturnUrl =
27 | // process.env.REDIRECT_URI + '/complete';
28 | const dsPingUrl = process.env.REDIRECT_URI + '/index';
29 | const smallLenderName = text.names.smallLenderName;
30 | const bigLenderName = text.names.bigLenderName;
31 | const loanBenchmark = 50000;
32 | const brandName = text.envelope.brandName;
33 | const defaultBrandLanguage = 'zh_CN';
34 |
35 | /**
36 | * Controller that creates an envelope and returns a view URL for an
37 | * embedded signing session.
38 | */
39 | const createController = async (req, res, next) => {
40 | // Check the access token, which will also update the token
41 | // if it is expired
42 | await checkToken(req);
43 | // Construct arguments
44 | const { body } = req;
45 | // Colors for branding
46 | const colors = [
47 | { name: 'buttonPrimaryBackground', value: '#dad1e9' },
48 | { name: 'buttonPrimaryText', value: '#333333' },
49 | { name: 'headerBackground', value: '#674ea7' },
50 | { name: 'headerText', value: '#ffffff' },
51 | ];
52 |
53 | const envelopeArgs = {
54 | signerEmail: body.signerEmail,
55 | signerName: body.signerName,
56 | contractAmount: body.contractAmount,
57 | status: 'sent',
58 | docFile: path.resolve(docsPath, docFile),
59 |
60 | // Embedded signing arguments
61 | signerClientId: signerClientId,
62 | dsReturnUrl: dsReturnUrl,
63 | dsPingUrl: dsPingUrl,
64 |
65 | // Loan specific arguments
66 | smallLenderName: smallLenderName,
67 | bigLenderName: bigLenderName,
68 | loanBenchmark: loanBenchmark,
69 |
70 | // Branding arguments
71 | brandName: brandName,
72 | defaultBrandLanguage: defaultBrandLanguage,
73 | colors: colors,
74 | };
75 | const args = {
76 | accessToken: req.session.accessToken,
77 | basePath: req.session.basePath,
78 | accountId: req.session.accountId,
79 | envelopeArgs: envelopeArgs,
80 | };
81 |
82 | let results = null;
83 |
84 | // Send the envelope to signer
85 | try {
86 | // Verify that the user has conditional routing and document visibility
87 | // enabled on their account.
88 | const conditionalRoutingEnabled = await hasConditionalRoutingEnabled(args);
89 | const documentVisibilityEnabled = await hasDocumentVisibilityEnabled(args);
90 | if (conditionalRoutingEnabled === 'false') {
91 | throw new AppError(403, errorText.conditionalRoutingNotEnabled);
92 | } else if (documentVisibilityEnabled === 'false') {
93 | throw new AppError(403, errorText.documentVisibilityNotEnabled);
94 | }
95 |
96 | // Step 1 start
97 | // Create brand for envelope
98 | results = await createBrand(args);
99 | envelopeArgs.brandId = results;
100 | // Step 1 end
101 |
102 | // Step 2 start
103 | // Get the envelope definition for the envelope
104 | const envelopeDef = makeLoanApplicationEnvelope(args.envelopeArgs);
105 | // Step 2 end
106 |
107 | // Step 3 start
108 | // Send the envelope and get the envelope ID
109 | const envelopeId = await sendEnvelope(envelopeDef, args);
110 | // Step 3 end
111 | // const envelopeId = '1c450b46-afa2-4b41-bfd2-d4e2e79fd402'
112 |
113 | // Step 4 start
114 | // Get recipient view URL for embedded signing
115 | const viewUrl = await getRecipientViewUrl(envelopeId, args);
116 | // const viewUrl = 'https://blog.csdn.net/ababab12345/article/details/124104779'
117 | // Set results
118 | results = { envelopeId: envelopeId, redirectUrl: viewUrl };
119 | } catch (error) {
120 | console.log('Error sending the envelope.');
121 | next(error);
122 | }
123 |
124 | if (results) {
125 | // Save envelope ID and signer name for later use
126 | req.session.loanAppEnvelopeId = results.envelopeId;
127 | req.session.loanAppSignerName = body.signerName;
128 |
129 | // Send back redirect URL for embedded signing
130 | res.status(200).send(results.redirectUrl);
131 | // Step 4 end
132 | }
133 | };
134 |
135 |
136 | /**
137 | * Gets the loan amount the user inputted in their loan application envelope
138 | * and return the lender name based on that.
139 | */
140 | const submitLoanController = async (req, res, next) => {
141 | // Check the access token, which will also update the token
142 | // if it is expired
143 | await checkToken(req);
144 | // Create args
145 | const args = {
146 | accessToken: req.session.accessToken,
147 | basePath: req.session.basePath,
148 | accountId: req.session.accountId,
149 |
150 | // Envelope tab related args
151 | envelopeId: req.session.loanAppEnvelopeId, // the last submitted envelopeId
152 | signerName: req.session.loanAppSignerName, // last submitted signer name
153 | tabName: 'loanAmount', // the name of the tab that we want the value of
154 | };
155 | let results = null;
156 |
157 | // Get the tab data
158 | try {
159 | results = await getEnvelopeTabData(args);
160 | } catch (error) {
161 | console.log('Error getting tab/form data.');
162 | next(error);
163 | }
164 |
165 | if (results) {
166 | // Based on the loan amount, return lender name
167 | if (parseFloat(results) < loanBenchmark) {
168 | res.status(200).send(smallLenderName);
169 | } else {
170 | res.status(200).send(bigLenderName);
171 | }
172 | }
173 | };
174 |
175 | module.exports = {
176 | createController,
177 | submitLoanController,
178 | };
179 |
--------------------------------------------------------------------------------
/server/controllers/jwtController.js:
--------------------------------------------------------------------------------
1 | // JWT flow:
2 | // 1. Create consent URI and obtain user consent.
3 | // 2. Construct JWT using the IK and User ID, scope, RSA public and private key.
4 | // 3. Send POST containing the JWT to DS_AUTH_SERVER to get access token.
5 | // 4. Using the access token, send a POST to get the user's base URI (account_id + base_uri).
6 | // 5. Now you can use the access token and base URI to make API calls.
7 | // When the access token expires, create a new JWT and request a new access token.
8 | const path = require('path');
9 | require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
10 | const eSignSdk = require('docusign-esign');
11 | const fs = require('fs'); // Used to parse RSA key
12 | const dayjs = require('dayjs'); // Used to set and determine a token's expiration date
13 | const oAuth = eSignSdk.ApiClient.OAuth;
14 | const restApi = eSignSdk.ApiClient.RestApi;
15 |
16 | // Constants
17 | const rsaKey = fs.readFileSync(path.resolve(__dirname, '../../private.key'));
18 | const jwtLifeSec = 60 * 60; // Request lifetime of JWT token is 60 minutes
19 | const scopes = 'signature';
20 |
21 | // For production environment, change "DEMO" to "PRODUCTION"
22 | const basePath = restApi.BasePath.DEMO; // https://demo.docusign.net/restapi
23 | const oAuthBasePath = oAuth.BasePath.DEMO; // account-d.docusign.com
24 |
25 | /**
26 | * Creates and sends a JWT token using the integration key, user ID, scopes and RSA key.
27 | * Then stores the returned access token and expiration date.
28 | */
29 | const getToken = async (req) => {
30 | // Get API client and set the base paths
31 | const eSignApi = new eSignSdk.ApiClient();
32 | eSignApi.setOAuthBasePath(oAuthBasePath);
33 |
34 | // Request a JWT token
35 | let results;
36 |
37 | results = await eSignApi.requestJWTUserToken(
38 | process.env.INTEGRATION_KEY,
39 | process.env.USER_ID,
40 | scopes,
41 | rsaKey,
42 | jwtLifeSec
43 | );
44 |
45 | // Save the access token and the expiration timestamp
46 | const expiresAt = dayjs().add(results.body.expires_in, 's'); // TODO: subtract tokenReplaceMin?
47 | req.session.accessToken = results.body.access_token;
48 | req.session.tokenExpirationTimestamp = expiresAt;
49 | };
50 |
51 | /**
52 | * Checks to see that the current access token is still valid, and if not,
53 | * updates the token.
54 | * Must be called before every DocuSign API call.
55 | */
56 | const checkToken = async (req) => {
57 | try {
58 | const noToken =
59 | !req.session.accessToken || !req.session.tokenExpirationTimestamp,
60 | currentTime = dayjs(),
61 | bufferTime = 1; // One minute buffer time
62 |
63 | // Check to see if we have a token or if the token is expired
64 | let needToken =
65 | noToken ||
66 | dayjs(req.session.tokenExpirationTimestamp)
67 | .subtract(bufferTime, 'm')
68 | .isBefore(currentTime);
69 |
70 | // Update the token if needed
71 | if (needToken) {
72 | await getToken(req);
73 | }
74 | } catch (error) {
75 | if (
76 | error.response.body.error &&
77 | error.response.body.error === 'consent_required'
78 | ) {
79 | throw new Error('Consent required');
80 | } else {
81 | throw error;
82 | }
83 | }
84 | };
85 |
86 | /**
87 | * Gets the account ID, account name, and base path of the user using the access token.
88 | */
89 | const getUserInfo = async (req) => {
90 | // Get API client
91 | const eSignApi = new eSignSdk.ApiClient(),
92 | targetAccountId = process.env.targetAccountId,
93 | baseUriSuffix = '/restapi';
94 | eSignApi.setOAuthBasePath(oAuthBasePath);
95 |
96 | // Get user info using access token
97 | const results = await eSignApi.getUserInfo(req.session.accessToken);
98 |
99 | let accountInfo;
100 | if (!Boolean(targetAccountId)) {
101 | // Find the default account
102 | accountInfo = results.accounts.find(
103 | (account) => account.isDefault === 'true'
104 | );
105 | } else {
106 | // Find the matching account
107 | accountInfo = results.accounts.find(
108 | (account) => account.accountId == targetAccountId
109 | );
110 | }
111 | if (typeof accountInfo === 'undefined') {
112 | throw new Error(`Target account ${targetAccountId} not found!`);
113 | }
114 |
115 | // Save user information in session.
116 | req.session.accountId = accountInfo.accountId;
117 | req.session.basePath = accountInfo.baseUri + baseUriSuffix;
118 | };
119 |
120 | /**
121 | * First checks if there is already a valid access token, updates it if it's expired,
122 | * then gets some user info. If the user has never provided consent, then they are
123 | * redirected to a login screen.
124 | */
125 | const login = async (req, res, next) => {
126 | try {
127 | // As long as the user has attempted to login before, they have either successfully
128 | // logged in or was redirected to the consent URL and then redirected back to the
129 | // app. Only set the user to logged out if an unknown error occurred during the
130 | // login process.
131 | req.session.isLoggedIn = true;
132 | await checkToken(req);
133 | await getUserInfo(req);
134 | res.status(200).send('Successfully logged in.');
135 | } catch (error) {
136 | // User has not provided consent yet, send the redirect URL to user.
137 | if (error.message === 'Consent required') {
138 | let consent_scopes = scopes + '%20impersonation',
139 | consent_url =
140 | `${process.env.DS_OAUTH_SERVER}/oauth/auth?response_type=code&` +
141 | `scope=${consent_scopes}&client_id=${process.env.INTEGRATION_KEY}&` +
142 | `redirect_uri=${process.env.REDIRECT_URI_HOME}`;
143 |
144 | res.status(210).send(consent_url);
145 | } else {
146 | req.session.isLoggedIn = false;
147 | next(error);
148 | }
149 | }
150 | };
151 |
152 | /**
153 | * Logs the user out by destroying the session.
154 | */
155 | const logout = (req, res) => {
156 | req.session = null;
157 | console.log('Successfully logged out!');
158 | res.status(200).send('Success: you have logged out');
159 | };
160 |
161 | /**
162 | * Sends back "true" if the user is logged in, false otherwise.
163 | */
164 | const isLoggedIn = async (req, res) => {
165 | // console.log(1, req.session)
166 | // let isLoggedIn;
167 | // try {
168 | // await checkToken(req);
169 | // isLoggedIn = true;
170 | // } catch (error) {
171 | // isLoggedIn = false;
172 | // }
173 | // res.status(200).send(isLoggedIn);
174 | let isLoggedIn;
175 | if (req.session.isLoggedIn === undefined) {
176 | isLoggedIn = false;
177 | } else {
178 | isLoggedIn = req.session.isLoggedIn;
179 | }
180 |
181 | res.status(200).send(isLoggedIn);
182 | };
183 |
184 | module.exports = {
185 | checkToken,
186 | login,
187 | logout,
188 | isLoggedIn,
189 | getUserInfo,
190 | };
191 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import React, { useEffect, useRef, useState } from 'react';
3 | import Home from './pages/Home';
4 | import Login from './pages/Login';
5 | import Passport from './pages/PassPort/Passport';
6 | import ErrorPage from './pages/ErrorPage';
7 | import {
8 | BrowserRouter as Router,
9 | Routes,
10 | Route,
11 | Navigate,
12 | } from 'react-router-dom';
13 | import SmallBusinessLoan from './pages/SmallBusinessLoan/SmallBusinessLoan';
14 | import Success from './pages/Success';
15 | import SubmittedLoan from './pages/SmallBusinessLoan/SubmittedLoan';
16 | import TrafficTicket from './pages/TrafficTicket/TrafficTicket';
17 | import UseCaseIndex from './pages/UseCaseIndex';
18 | import SubmittedTrafficTicket from './pages/TrafficTicket/SubmittedTrafficTicket';
19 | import PageNotFound from './pages/PageNotFound';
20 | import axios from './api/request';
21 | import RequireAuth from './components/RequireAuth';
22 | import './assets/scss/main.scss';
23 | import Header from './components/Header';
24 | import WitnessStatement from './pages/TrafficTicket/WitnessStatement';
25 | import { handleError } from './api/apiHelper';
26 | import PassportSign from './pages/PassPort/PassportSign'
27 |
28 | function App() {
29 | let mountedRef = useRef(true);
30 | const [textContent, setTextContent] = useState('');
31 |
32 | useEffect(() => {
33 | // Load in the text content on page load.
34 | getTextContent();
35 |
36 | // Clean up
37 | return () => {
38 | mountedRef.current = false;
39 | };
40 | }, []);
41 |
42 | // GETs the static text content from the server that will be used to
43 | // populate the app.
44 | async function getTextContent() {
45 | try {
46 | let response = await axios.get('http://localhost:8085/assets/text.json');
47 |
48 | // Only set states if the component is mounted, otherwise return null.
49 | if (!mountedRef.current) return null;
50 |
51 | setTextContent(response.data);
52 | } catch (error) {
53 | console.log('Error getting static text asset.');
54 | console.log(error);
55 | const errorPageText = handleError(error);
56 | ;
57 | }
58 | }
59 |
60 | return (
61 | <>
62 | {textContent ? (
63 |
64 |
65 |
66 |
74 | }
75 | />
76 |
77 | }>
78 |
85 | }
86 | />
87 |
88 |
89 | }>
90 | }
93 | >
94 |
102 | }
103 | />
104 |
108 | }
109 | />
110 |
111 |
112 |
113 | }>
114 | }
117 | >
118 |
127 | }
128 | />
129 |
133 | }
134 | />
135 |
136 |
137 |
138 | }>
139 | }>
140 |
149 | }
150 | />
151 |
155 | }
156 | />
157 |
165 | }
166 | />
167 |
168 |
169 |
170 | }
173 | />
174 | } />
175 | }
178 | />
179 |
180 |
181 |
182 | ) : (
183 | // Display nothing while static assets are being loaded in.
184 | <>>
185 | )}
186 | >
187 | );
188 | }
189 |
190 | export default App;
191 |
--------------------------------------------------------------------------------
/server/controllers/trafficController.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { checkToken } = require('./jwtController');
3 | const text = require('../assets/public/text.json').trafficTicket.names;
4 | const {
5 | getIdvWorkflowId,
6 | } = require('../docusign/workflow');
7 | const { makeSmsEnvelope } = require('../docusign/envelopes/makeSmsEnvelope');
8 | const {
9 | getRecipientViewUrl,
10 | getEnvelopeTabData,
11 | } = require('../docusign/envelope');
12 | const {
13 | sendEnvelopeFromTemplate,
14 | } = require('../docusign/envelopByTemplate');
15 | const {
16 | hasSmsEnabled,
17 | hasCertifiedDeliveryEnabled,
18 | hasConditionalRoutingEnabled,
19 | } = require('../docusign/workflow');
20 | const errorText = require('../assets/errorText.json').api;
21 | const AppError = require('../utils/appError');
22 |
23 | // Set constants
24 | const signerClientId = '1000'; // The id of the signer within this application.
25 | const docsPath = path.resolve(__dirname, '../docusign/pdf');
26 | const docFile2 = 'PoliceWitnessStatement.pdf';
27 | const dsReturnUrl =
28 | process.env.REDIRECT_URI + '/receive-traffic-ticket/submitted-ticket';
29 | const dsPingUrl = process.env.REDIRECT_URI + '/';
30 |
31 | /**
32 | * Controller that creates an envelope and returns a view URL for an
33 | * embedded signing session.
34 | */
35 | const createController = async (req, res, next) => {
36 | // Check the access token, which will also update the token
37 | // if it is expired
38 | await checkToken(req);
39 |
40 | // Construct arguments
41 | const { body } = req;
42 |
43 | const envelopeArgs = {
44 | signerEmail: body.signerEmail,
45 | signerName: body.signerName,
46 | countryCode: body.countryCode,
47 | phoneNumber: body.phoneNumber,
48 | templateId: '2f9bf387-68e4-4168-b90a-df821830161c', // docusign定义的模板id
49 | status: 'sent',
50 |
51 | // Embedded signing arguments
52 | signerClientId: signerClientId,
53 | dsReturnUrl: dsReturnUrl,
54 | dsPingUrl: dsPingUrl,
55 | };
56 | const args = {
57 | accessToken: req.session.accessToken,
58 | basePath: req.session.basePath,
59 | accountId: req.session.accountId,
60 | envelopeArgs: envelopeArgs,
61 | };
62 |
63 | let results = null;
64 |
65 | // Send the envelope to signer
66 | try {
67 | // Verify that the user has payment related environment variables set up,
68 | // send to CertifiedDelivery and conditional routing enabled on their account.
69 | const certifiedDeliveryEnabled = await hasCertifiedDeliveryEnabled(args);
70 | const conditionalRoutingEnabled = await hasConditionalRoutingEnabled(args);
71 | if (
72 | !process.env.PAYMENT_GATEWAY_ACCOUNT_ID ||
73 | !process.env.PAYMENT_GATEWAY_NAME ||
74 | !process.env.PAYMENT_GATEWAY_DISPLAY_NAME
75 | ) {
76 | throw new AppError(403, errorText.paymentConfigsUndefined);
77 | } else if (certifiedDeliveryEnabled === 'false') {
78 | throw new AppError(403, errorText.certifiedDeliveryNotEnabled);
79 | } else if (conditionalRoutingEnabled === 'false') {
80 | throw new AppError(403, errorText.conditionalRoutingNotEnabled);
81 | }
82 |
83 | // Step 1 start
84 | // Get the workflowId and add it to envelopeArgs
85 | const workflowId = await getIdvWorkflowId(args);
86 | // console.log(6666, workflowId)
87 | // If IDV is not enabled in the account, send back error message.
88 | if (workflowId === null) {
89 | throw new AppError(403, errorText.idvNotEnabled);
90 | }
91 |
92 | args.envelopeArgs.workflowId = workflowId;
93 |
94 | // Step 2 start
95 | // Send the envelope and get the envelope ID
96 | const envelopeId = await sendEnvelopeFromTemplate(args);
97 | // Step 2 end
98 |
99 | // Step 3 start
100 | // Get recipient view URL for embedded signing
101 | const viewUrl = await getRecipientViewUrl(envelopeId, args);
102 |
103 | // Set results
104 | results = { envelopeId: envelopeId, redirectUrl: viewUrl };
105 | } catch (error) {
106 | console.log('Error sending the envelope.');
107 | next(error);
108 | }
109 |
110 | if (results) {
111 | // Save envelope ID and signer name for later use
112 | req.session.ticketEnvelopeId = results.envelopeId;
113 | req.session.ticketSignerName = body.signerName;
114 | res.status(200).send(results.redirectUrl);
115 | // Step 3 end
116 | }
117 | };
118 |
119 | /**
120 | * Gets and returns what the user chose to do with the ticket
121 | * (plead not guilty/no contest and paid, request mitigation hearing,
122 | * or request contested hearing).
123 | */
124 | const submitTrafficController = async (req, res, next) => {
125 | // Check the access token, which will also update the token
126 | // if it is expired
127 | await checkToken(req);
128 |
129 | // Create args
130 | const args = {
131 | accessToken: req.session.accessToken,
132 | basePath: req.session.basePath,
133 | accountId: req.session.accountId,
134 |
135 | // Envelope tab related args
136 | envelopeId: req.session.ticketEnvelopeId, // the last submitted envelopeId
137 | signerName: req.session.ticketSignerName, // last submitted signer name
138 | tabName: 'ticketOption', // the name of the tab that we want the value of
139 | };
140 | let results = null;
141 |
142 | // Get the tab data
143 | try {
144 | results = await getEnvelopeTabData(args);
145 | } catch (error) {
146 | console.log('Error getting tab/form data.');
147 | next(error);
148 | }
149 |
150 | if (results) {
151 | res.status(200).send(results);
152 | }
153 | };
154 |
155 | /**
156 | * Sends an envelope via SMS delivery to the phone provided in the request.
157 | */
158 | const smsTrafficController = async (req, res, next) => {
159 | // Check the access token, which will also update the token
160 | // if it is expired
161 | await checkToken(req);
162 |
163 | // Construct arguments
164 | const { body } = req;
165 |
166 | const envelopeArgs = {
167 | signerName: body.signerName,
168 | status: 'sent',
169 | docFile: path.resolve(docsPath, docFile2),
170 |
171 | // SMS args
172 | countryCode: body.countryCode,
173 | phoneNumber: body.phoneNumber,
174 | };
175 | const args = {
176 | accessToken: req.session.accessToken,
177 | basePath: req.session.basePath,
178 | accountId: req.session.accountId,
179 | envelopeArgs: envelopeArgs,
180 | };
181 |
182 | let results = null;
183 |
184 | try {
185 | // Verify that the user has the SMS Delivery feature enabled on
186 | // their account first.
187 | const smsEnabled = await hasSmsEnabled(args);
188 | if (smsEnabled === 'false') {
189 | throw new AppError(403, errorText.smsNotEnabled);
190 | }
191 |
192 | // Step 1 start
193 | // Get the envelope definition for the envelope
194 | const envelopeDef = makeSmsEnvelope(args.envelopeArgs);
195 | // Step 1 end
196 |
197 | // Step 2 start
198 | // Send the envelope and get the envelope ID
199 | const envelopeId = await sendEnvelope(envelopeDef, args);
200 | // Step 2 end
201 |
202 | // Set results
203 | results = { envelopeId: envelopeId };
204 | } catch (error) {
205 | console.log('Error SMS delivery.');
206 | next(error);
207 | }
208 |
209 | if (results) {
210 | req.session.ticketSmsEnvelopeId = results.envelopeId;
211 | res.status(200).send('Envelope successfully sent!');
212 | }
213 | };
214 |
215 | module.exports = {
216 | createController,
217 | submitTrafficController,
218 | smsTrafficController,
219 | };
220 |
--------------------------------------------------------------------------------
/server/docusign/useTemplate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | * Example 009: Send envelope using a template
4 | * @author DocuSign
5 | */
6 |
7 | const docusign = require("docusign-esign");
8 | const signerClientId = '1000';
9 |
10 | /**
11 | * This function does the work of creating the envelope
12 | * @param {object} args object
13 | */
14 | const sendEnvelopeFromTemplate = async (args) => {
15 | // Data for this method
16 | // args.basePath
17 | // args.accessToken
18 | // args.accountId
19 |
20 | let dsApiClient = new docusign.ApiClient();
21 | dsApiClient.setBasePath(args.basePath);
22 | dsApiClient.addDefaultHeader("Authorization", "Bearer " + args.accessToken);
23 | let envelopesApi = new docusign.EnvelopesApi(dsApiClient);
24 |
25 | // await getTemplatDocument(args)
26 | // Step 1. Make the envelope request body
27 | let envelope = await makeEnvelope(args.envelopeArgs);
28 |
29 | // Step 2. call Envelopes::create API method
30 | // Exceptions will be caught by the calling function
31 | let results = await envelopesApi.createEnvelope(args.accountId, {
32 | envelopeDefinition: envelope,
33 | });
34 | let envelopeId = results.envelopeId;
35 | console.log(`Envelope was created. EnvelopeId ${envelopeId}`);
36 |
37 | // envelopesApi.createDocumentTabs(args.accountId, envelopeId, '1', tabs);
38 |
39 | return envelopeId;
40 | };
41 |
42 | /**
43 | * Creates envelope from the template
44 | * @function
45 | * @param {Object} args object
46 | * @returns {Envelope} An envelope definition
47 | * @private
48 | */
49 | async function makeEnvelope(args) {
50 |
51 | // create the envelope definition
52 | let env = new docusign.EnvelopeDefinition();
53 | let signers = args.signers.map((item, i) => {
54 | const newItem = {
55 | email: item.email,
56 | name: item.name,
57 | roleName: item.roleName,
58 | recipientId: item.recipientId,
59 | clientUserId: signerClientId,
60 | emailNotification: {
61 | supportedLanguage: 'zh_CN'
62 | }
63 | }
64 | if (item.roleName === "partyA") {
65 | newItem.tabs = {
66 | textTabs: [
67 | {
68 | documentId: "1",
69 | locked: true,
70 | maxLength: "4000",
71 | originalValue: "",
72 | pageNumber: "1",
73 | recipientId: "d749512b-217d-46e6-b644-051286852b30",
74 | requireAll: "false",
75 | required: true,
76 | shared: true,
77 | tabId: "ba7d20bf-86b8-4fed-9711-cfaa2dfc7298",
78 | tabLabel: "文本 7cf4671c-597f-4180-aa19-b382c0b5bd2e",
79 | tabType: "text",
80 | templateLocked: "false",
81 | templateRequired: "false",
82 | underline: "false",
83 | value: "预填充字段1"
84 | },
85 | // {
86 | // documentId: "1",
87 | // locked: true,
88 | // maxLength: "4000",
89 | // originalValue: "",
90 | // pageNumber: "1",
91 | // recipientId: "d749512b-217d-46e6-b644-051286852b30",
92 | // requireAll: "false",
93 | // required: true,
94 | // shared: true,
95 | // tabId: "ef13a215-1723-4f20-95e9-f7ec99e781ce",
96 | // tabLabel: "文本 87af5e7c-abc0-4b70-8db3-54a0b9336efc",
97 | // tabType: "text",
98 | // templateLocked: "false",
99 | // templateRequired: "false",
100 | // underline: "false",
101 | // value: "预填充字段2"
102 | // }
103 | ]
104 | }
105 | } else {
106 | // newItem.clientUserId = item.signerClientId
107 | }
108 | if (item.identityVerification) { // 如果有短信验证
109 | newItem.identityVerification = {
110 | workflowId: args.workflowId,
111 | steps: null,
112 | "inputOptions":
113 | [{
114 | "name":"phone_number_list",
115 | "valueType":"PhoneNumberList",
116 | "phoneNumberList":[
117 | {
118 | "countryCode":item.countryCode,
119 | "code":"1",
120 | "number":item.phoneNumber
121 | }
122 | ]
123 | }],
124 | "idCheckConfigurationName":""
125 | }
126 | }
127 | return newItem
128 | })
129 |
130 | const compTemplate = docusign.CompositeTemplate.constructFromObject({
131 | compositeTemplateId: "1",
132 | serverTemplates: [
133 | docusign.ServerTemplate.constructFromObject({
134 | sequence: "1",
135 | templateId: args.templateId,
136 | }),
137 | ],
138 | // Add the roles via an inlineTemplate
139 | inlineTemplates: [
140 | docusign.InlineTemplate.constructFromObject({
141 | sequence: "2",
142 | recipients: {
143 | signers: signers,
144 | },
145 | }),
146 | ],
147 | });
148 |
149 | // Recipients object for the added document:
150 | // let recipientsAddedDoc = docusign.Recipients.constructFromObject({
151 | // signers: signers,
152 | // });
153 |
154 | // let doc1 = new docusign.Document(),
155 | // doc1b64 = Buffer.from(document1(args)).toString("base64");
156 | // doc1.documentBase64 = doc1b64;
157 | // doc1.name = "Appendix 1--Sales order"; // can be different from actual file name
158 | // doc1.fileExtension = "html";
159 | // doc1.documentId = "1";
160 |
161 | // // 增加附件, 如果要添加附件修改doc1
162 | // // create a composite template for the added document
163 | // let compTemplate2 = docusign.CompositeTemplate.constructFromObject({
164 | // compositeTemplateId: "2",
165 | // // Add the recipients via an inlineTemplate
166 | // inlineTemplates: [
167 | // docusign.InlineTemplate.constructFromObject({
168 | // sequence: "1",
169 | // recipients: recipientsAddedDoc,
170 | // }),
171 | // ],
172 | // document: doc1,
173 | // });
174 |
175 | env.compositeTemplates = [compTemplate]
176 | env.status = "sent"; // We want the envelope to be sent
177 | console.log(signers)
178 | return env;
179 | }
180 |
181 | function document1(args) {
182 | // Data for this method
183 | // args.signerEmail
184 | // args.signerName
185 | // args.ccEmail
186 | // args.ccName
187 | // args.item
188 | // args.quantity
189 |
190 | return `
191 |
192 |
193 |
194 |
195 |
196 |
197 | World Wide Corp
199 |
200 | | 表格1 | 表格2 | 表格3 |
201 | | 表格1 | 表格2 | 表格3 |
202 |
203 | Ordered by ${args.signerName}
204 | Email: ${args.signerEmail}
205 | Copy to: ${args.ccName}, ${args.ccEmail}
206 | Item: ${args.item}, quantity: ${args.quantity} at market price.
207 |
208 | Candy bonbon pastry jujubes lollipop wafer biscuit biscuit. Topping brownie sesame snaps sweet roll pie. Croissant danish biscuit soufflé caramels jujubes jelly. Dragée danish caramels lemon drops dragée. Gummi bears cupcake biscuit tiramisu sugar plum pastry. Dragée gummies applicake pudding liquorice. Donut jujubes oat cake jelly-o. Dessert bear claw chocolate cake gummies lollipop sugar plum ice cream gummies cheesecake.
209 |
210 |
211 | Agreed: **signature_1**/
212 |
213 |
214 | `;
215 | }
216 |
217 | module.exports = { sendEnvelopeFromTemplate };
218 |
--------------------------------------------------------------------------------
/client/src/components/Form.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useForm } from 'react-hook-form';
3 | import { ErrorMessage } from '@hookform/error-message';
4 | import { useNavigate } from 'react-router-dom';
5 | import Popup from './Popup';
6 | import UserProfile from './UserProfile';
7 |
8 | function Form({
9 | avatarUrl,
10 | userRoleName,
11 | includeMoreInfo = true,
12 | text,
13 | userFlowText,
14 | includePhone,
15 | onSubmit,
16 | placeholderName,
17 | nameLabel,
18 | submitted = false,
19 | isPrefiled,
20 | }) {
21 | let navigate = useNavigate();
22 | const [isOpen, setIsOpen] = useState(false);
23 | const {
24 | register,
25 | handleSubmit,
26 | formState: { errors },
27 | } = useForm();
28 |
29 | const ErrorMessageContainer = ({ children }) => (
30 | {children}
31 | );
32 |
33 | return (
34 | <>
35 |
227 |
228 | {isOpen && (
229 | {
232 | setIsOpen(!isOpen);
233 | }}
234 | />
235 | )}
236 | >
237 | );
238 | }
239 |
240 | export default Form;
241 |
--------------------------------------------------------------------------------
/client/src/pages/PassPort/Passport.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { handleError, sendRequest } from '../../api/apiHelper';
3 | import { useNavigate } from 'react-router-dom';
4 | import axios from '../../api/request'
5 |
6 | function Passport({ text }) {
7 | let navigate = useNavigate();
8 | const [requesting, setRequesting] = useState(false);
9 | const [templateList, setTemplateList] = useState([])
10 | const [singers, setSingers] = useState([])
11 | const [templateId, setTemplateId] = useState('')
12 | const [envelopeId, setEnvelopId] = useState('')
13 |
14 | const getTemplate = async () => {
15 | try {
16 | const {data} = await axios('/template/getTemplates');
17 | setTemplateList(data)
18 |
19 | } catch (error) {
20 | console.log(error);
21 | const errorPageText = handleError(error);
22 | navigate('/error', { state: errorPageText });
23 | }
24 | }
25 |
26 | useEffect(() => {
27 | getTemplate()
28 | }, [])
29 |
30 | // 选择模板 获取模板配置的签约人角色
31 | const changeTemplate = async(e) => {
32 | setTemplateId(e.target.value)
33 | const body = {
34 | templateId: e.target.value,
35 | };
36 | try {
37 | const {data} = await sendRequest('/template/getSigners', body);
38 | const arr = data.map((item, index) => {
39 | return {
40 | ...item,
41 | id: `${Date.now()}${index}`
42 | }
43 | })
44 | setSingers(arr)
45 | // console.log(data)
46 |
47 | } catch (error) {
48 | console.log(error);
49 | const errorPageText = handleError(error);
50 | navigate('/error', { state: errorPageText });
51 | }
52 | }
53 |
54 | // 签约人数据改变
55 | const changeTable = (e, index) => {
56 | const {value, name} = e.target
57 | const arr = singers
58 | arr[index][name] = value
59 | // arr[index].id = `${Date.now()}${index}`
60 | setSingers(arr)
61 | }
62 |
63 | // Sends POST request to server to send envelope based on the
64 | // info the user provided in the form.
65 | async function handleSubmit() {
66 | console.log(templateId, singers)
67 | setRequesting(true);
68 |
69 | // Make request body
70 | const body = {
71 | templateId,
72 | signers: singers,
73 | };
74 |
75 | // Send request to server
76 | try {
77 | const response = await sendRequest('/template/sendByTemplate', body);
78 | console.log(response.data);
79 | setRequesting(false);
80 | setEnvelopId(response.data)
81 | // Redirect to success screen
82 | // navigate('/success');
83 | } catch (error) {
84 | console.log(error);
85 | const errorPageText = handleError(error);
86 | navigate('/error', { state: errorPageText });
87 | }
88 | }
89 | // 生成签约视图
90 | const toSign = async (item) => {
91 | // Make request body
92 | const body = {
93 | ...item,
94 | envelopeId,
95 | };
96 |
97 | try {
98 | const response = await sendRequest('/template/getViewByEnvelope', body);
99 | // const response = await sendRequest('/template/sendEmailByTemplate', body);
100 | if (response.status === 200) {
101 | window.location = response.data;
102 | }
103 | } catch (error) {
104 | console.log(error);
105 | const errorPageText = handleError(error);
106 | navigate('/error', { state: errorPageText });
107 | }
108 | }
109 |
110 | // 获取模板tabs
111 | const getTabs = async () => {
112 | const body = {
113 | documentId: '1',
114 | templateId,
115 | // envelopeId: '112308ca-d225-4016-9854-9a675925e30e',
116 | };
117 | try {
118 | const {data} = await sendRequest('/template/getTemplateDocumentTabs', body);
119 | console.log(data)
120 | } catch (error) {
121 | console.log(error);
122 | }
123 | }
124 |
125 | return (
126 |
127 |
128 |
129 |
{text.title}
130 |
136 |
137 |
138 |
144 |
236 |
237 | {envelopeId ?
238 |
239 |
240 |
241 |
{envelopeId}
242 |
243 |
244 |
245 |
246 | {singers.map(item =>
247 | -
248 | {item.name}你有一份待签约文件
249 |
250 |
251 | )}
252 |
253 |
: null}
254 |
255 |
256 |
257 |
258 | );
259 | }
260 |
261 | export default Passport;
262 |
--------------------------------------------------------------------------------
/server/controllers/templateContraoller.js:
--------------------------------------------------------------------------------
1 | const {
2 | sendEnvelopeFromTemplate,
3 | } = require('../docusign/useTemplate')
4 | const { getIdvWorkflowId } = require('../docusign/workflow');
5 | const { checkToken } = require('./jwtController');
6 | const {
7 | getRecipientViewUrl,
8 | } = require('../docusign/envelope');
9 | const {
10 | sendEmailByEnvelopeId,
11 | } = require('../docusign/envelopByTemplate');
12 | const {
13 | getTemplateList,
14 | getTemplatSigners,
15 | getenvelops,
16 | getenvelopDocuments,
17 | getenvelopPdf,
18 | getenvelopDocumentImages,
19 | getTemplateDocumentTabs
20 | } = require('../docusign/templates')
21 |
22 | // Set constants
23 | const signerClientId = '1000'; // The id of the signer within this application.
24 | const dsReturnUrl =
25 | process.env.REDIRECT_URI + '/apply-for-passport/passport-sign';
26 | const dsPingUrl = process.env.REDIRECT_URI + '/index';
27 |
28 | // 创建信封
29 | const templateController = async (req, res, next) => {
30 | // Check the access token, which will also update the token
31 | // if it is expired
32 | await checkToken(req);
33 | const {body} = req
34 |
35 | // Create args
36 | const envelopeArgs = {
37 | signers: body.signers,
38 | templateId: body.templateId,
39 |
40 | // Embedded signing arguments
41 | dsReturnUrl: dsReturnUrl,
42 | dsPingUrl: dsPingUrl,
43 | };
44 | const args = {
45 | accessToken: req.session.accessToken,
46 | basePath: req.session.basePath,
47 | accountId: req.session.accountId,
48 | templateId: body.templateId,
49 | envelopeArgs: envelopeArgs,
50 | };
51 |
52 | let results = null;
53 |
54 | // Get the tab data
55 | try {
56 | // Step 1 start
57 | // Get the workflowId and add it to envelopeArgs
58 | const workflowId = await getIdvWorkflowId(args);
59 | args.envelopeArgs.workflowId = workflowId;
60 | // console.log(22, workflowId)
61 | const envelopeId = await sendEnvelopeFromTemplate(args);
62 |
63 | // const viewUrl = await getRecipientViewUrl(envelopeId, args);
64 |
65 | // Set results
66 | results = { envelopeId: envelopeId };
67 | } catch (error) {
68 | console.log('Error sending the envelope.');
69 | next(error);
70 | }
71 |
72 | if (results) {
73 | // Save envelope ID and signer name for later use
74 | req.session.loanAppEnvelopeId = results.envelopeId;
75 | // req.session.loanAppSignerName = body.signerName;
76 |
77 | // Send back redirect URL for embedded signing
78 | res.status(200).send(results.envelopeId);
79 | }
80 | };
81 |
82 | // 创建收件人视图
83 | const templateViewController = async (req, res, next) => {
84 | // Check the access token, which will also update the token
85 | // if it is expired
86 | await checkToken(req);
87 | const {body} = req
88 | // Create args
89 | const envelopeArgs = {
90 | signerEmail: body.email,
91 | signerName: body.name,
92 | roleName: body.roleName,
93 |
94 | // Embedded signing arguments
95 | signerClientId: signerClientId,
96 | dsReturnUrl: dsReturnUrl,
97 | dsPingUrl: dsPingUrl,
98 |
99 | };
100 | const args = {
101 | accessToken: req.session.accessToken,
102 | basePath: req.session.basePath,
103 | accountId: req.session.accountId,
104 | envelopeArgs: envelopeArgs,
105 | };
106 |
107 | let results = null;
108 |
109 | // Get the tab data
110 | try {
111 | const viewUrl = await getRecipientViewUrl(body.envelopeId, args);
112 |
113 | // Set results
114 | // results = { envelopeId: envelopeId, redirectUrl: viewUrl };
115 | results = { redirectUrl: viewUrl };
116 | } catch (error) {
117 | console.log('Error sending the envelope.');
118 | next(error);
119 | }
120 |
121 | if (results) {
122 | // Save envelope ID and signer name for later use
123 | req.session.loanAppSignerName = body.signerName;
124 |
125 | // Send back redirect URL for embedded signing
126 | res.status(200).send(results.redirectUrl);
127 | }
128 | };
129 |
130 | // 获取模板列表
131 | const templateListController = async (req, res, next) => {
132 | // Check the access token, which will also update the token
133 | // if it is expired
134 | await checkToken(req);
135 |
136 | // Create args
137 | const args = {
138 | accessToken: req.session.accessToken,
139 | basePath: req.session.basePath,
140 | accountId: req.session.accountId,
141 |
142 | };
143 | let results = null;
144 |
145 | // Get the tab data
146 | try {
147 | results = await getTemplateList(args);
148 | } catch (error) {
149 | console.log('Error getting template data.');
150 | next(error);
151 | }
152 |
153 | if (results) {
154 | res.status(200).send(results);
155 | }
156 | };
157 |
158 | // 获取模板签约人列表
159 | const templateSignersController = async (req, res, next) => {
160 | // Check the access token, which will also update the token
161 | // if it is expired
162 | await checkToken(req);
163 | const {body} = req
164 |
165 | // Create args
166 | const args = {
167 | accessToken: req.session.accessToken,
168 | basePath: req.session.basePath,
169 | accountId: req.session.accountId,
170 | templateId: body.templateId
171 | };
172 | let results = null;
173 |
174 | // Get the tab data
175 | try {
176 | results = await getTemplatSigners(args);
177 | } catch (error) {
178 | console.log('Error getting template data.');
179 | next(error);
180 | }
181 |
182 | if (results) {
183 | res.status(200).send(results);
184 | }
185 | };
186 |
187 | // 获取信封列表
188 | const envelopsController = async (req, res, next) => {
189 | // Check the access token, which will also update the token
190 | // if it is expired
191 | await checkToken(req);
192 | const {body} = req
193 |
194 | // Create args
195 | const args = {
196 | accessToken: req.session.accessToken,
197 | basePath: req.session.basePath,
198 | accountId: req.session.accountId,
199 | folderIds: body.folderIds,
200 | fromDate: body.fromDate
201 | };
202 | let results = null;
203 |
204 | // Get the tab data
205 | try {
206 | results = await getenvelops(args);
207 | } catch (error) {
208 | console.log('Error getting template data.');
209 | next(error);
210 | }
211 |
212 | if (results) {
213 | res.status(200).send(results);
214 | }
215 | };
216 |
217 | // 获取信封文件pdf
218 | const envelopPdfController = async (req, res, next) => {
219 | // Check the access token, which will also update the token
220 | // if it is expired
221 | await checkToken(req);
222 | const {body} = req
223 |
224 | // Create args
225 | const args = {
226 | accessToken: req.session.accessToken,
227 | basePath: req.session.basePath,
228 | accountId: req.session.accountId,
229 | envelopeId: body.envelopeId
230 | };
231 | let results = null;
232 |
233 | // Get the tab data
234 | try {
235 | results = await getenvelopPdf(args);
236 | } catch (error) {
237 | console.log('Error getting template data.');
238 | next(error);
239 | }
240 |
241 | if (results) {
242 | res.writeHead(200, {
243 | 'Content-Type': results.mimetype,
244 | 'Content-disposition': 'inline;filename=' + results.docName,
245 | 'Content-Length': results.fileBytes.length
246 | });
247 | res.end(results.fileBytes, 'binary')
248 | }
249 | };
250 |
251 | // 获取信封文件列表
252 | const envelopDocumentsController = async (req, res, next) => {
253 | // Check the access token, which will also update the token
254 | // if it is expired
255 | await checkToken(req);
256 | const {body} = req
257 |
258 | // Create args
259 | const args = {
260 | accessToken: req.session.accessToken,
261 | basePath: req.session.basePath,
262 | accountId: req.session.accountId,
263 | envelopeId: body.envelopeId
264 | };
265 | let results = null;
266 |
267 | // Get the tab data
268 | try {
269 | results = await getenvelopDocuments(args);
270 | } catch (error) {
271 | console.log('Error getting template data.');
272 | next(error);
273 | }
274 |
275 | if (results) {
276 | res.status(200).send(results);
277 | }
278 | };
279 |
280 | // 根据文档ID获取文档图片
281 | const envelopDocumentImagesController = async (req, res, next) => {
282 | // Check the access token, which will also update the token
283 | // if it is expired
284 | await checkToken(req);
285 | const {body} = req
286 |
287 | // Create args
288 | const args = {
289 | accessToken: req.session.accessToken,
290 | basePath: req.session.basePath,
291 | accountId: req.session.accountId,
292 | envelopeId: body.envelopeId,
293 | documentId: body.documentId,
294 | pageNumber: body.pageNumber,
295 | };
296 | let results = null;
297 |
298 | // Get the tab data
299 | try {
300 | results = await getenvelopDocumentImages(args);
301 | } catch (error) {
302 | console.log('Error getting template data.');
303 | next(error);
304 | }
305 |
306 | if (results) {
307 | res.status(200).send(results);
308 | }
309 | };
310 |
311 | // 根据文档ID获取文档tabs
312 | const templateDocumentTabsController = async (req, res, next) => {
313 | await checkToken(req);
314 | const {body} = req
315 |
316 | // Create args
317 | const args = {
318 | accessToken: req.session.accessToken,
319 | basePath: req.session.basePath,
320 | accountId: req.session.accountId,
321 | templateId: body.templateId,
322 | documentId: body.documentId,
323 | };
324 | let results = null;
325 |
326 | // Get the tab data
327 | try {
328 | results = await getTemplateDocumentTabs(args);
329 | } catch (error) {
330 | console.log('Error getting template data.');
331 | next(error);
332 | }
333 |
334 | if (results) {
335 | res.status(200).send(results);
336 | }
337 | };
338 |
339 | // 根据envelopeId发送信封邮件
340 | const templateEmailSendController = async (req, res, next) => {
341 | await checkToken(req);
342 | const {body} = req
343 |
344 | // Create args
345 | const args = {
346 | accessToken: req.session.accessToken,
347 | basePath: req.session.basePath,
348 | accountId: req.session.accountId,
349 | envelopeId: body.envelopeId,
350 | signerEmail: body.email,
351 | signerName: body.name,
352 | roleName: body.roleName,
353 | signerClientId: body.signerClientId,
354 | };
355 | let results = null;
356 |
357 | // Get the tab data
358 | try {
359 | results = await sendEmailByEnvelopeId(args);
360 | } catch (error) {
361 | console.log('Error send envelope.');
362 | next(error);
363 | }
364 |
365 | if (results) {
366 | res.status(200).send(results);
367 | }
368 | };
369 |
370 |
371 | module.exports = {
372 | templateController,
373 | templateViewController,
374 | templateListController,
375 | templateSignersController,
376 | envelopsController,
377 | envelopDocumentsController,
378 | envelopPdfController,
379 | envelopDocumentImagesController,
380 | templateDocumentTabsController,
381 | templateEmailSendController
382 | };
383 |
--------------------------------------------------------------------------------
/server/docusign/envelopes/makeLoanApplication.js:
--------------------------------------------------------------------------------
1 | const eSignSdk = require('docusign-esign');
2 | const fs = require('fs');
3 | const text = require('../../assets/public/text.json').smallBusinessLoan
4 | .envelope;
5 |
6 | /**
7 | * Creates and returns envelope definition for small business loan application.
8 | */
9 | function makeLoanApplicationEnvelope(args) {
10 | let smallLenderName = args.smallLenderName;
11 | let bigLenderName = args.bigLenderName;
12 | let loanBenchmark = args.loanBenchmark;
13 |
14 | /////////////// Create documents for envelope ///////////////
15 | // Read and create documents from file in the local directory
16 | let docPdfBytes = fs.readFileSync(args.docFile);
17 | // let doc2DocxBytes = fs.readFileSync(args.doc2File);
18 |
19 | let docb64 = Buffer.from(docPdfBytes).toString('base64');
20 | // let doc2b64 = Buffer.from(doc2DocxBytes).toString('base64');
21 |
22 | let doc = new eSignSdk.Document.constructFromObject({
23 | documentBase64: docb64,
24 | name: text.loan1DocName,
25 | fileExtension: 'pdf',
26 | documentId: '1',
27 | });
28 |
29 | // let doc2 = new eSignSdk.Document.constructFromObject({
30 | // documentBase64: doc2b64,
31 | // name: text.loan2DocName,
32 | // fileExtension: 'docx',
33 | // documentId: '2',
34 | // });
35 |
36 | /////////////// Create signHere tabs ///////////////
37 | let signHere1 = eSignSdk.SignHere.constructFromObject({
38 | "documentId":"1",
39 | "pageNumber":"1",
40 | "recipientId":"21990853",
41 | "tabId":"c20b225e-38af-436e-9134-40353ac08080",
42 | "tabType":"signhere",
43 | "templateLocked":"false",
44 | "templateRequired":"false",
45 | "xPosition":"191",
46 | "yPosition":"177",
47 | "name":"SignHere",
48 | "optional":"false",
49 | "scaleValue":"1",
50 | "tabLabel":"签名 d3e77886-86c4-4aa1-87f6-d57c05a3c49b",
51 | "stampType":"signature"
52 | });
53 |
54 | let signHere2 = eSignSdk.SignHere.constructFromObject({
55 | "documentId":"1",
56 | "pageNumber":"1",
57 | "recipientId":"56569690",
58 | "tabId":"8a6b48d4-5950-45a5-94c4-cfd32791bf7a",
59 | "tabType":"signhere",
60 | "templateLocked":"false",
61 | "templateRequired":"false",
62 | "xPosition":"280",
63 | "yPosition":"176",
64 | "name":"SignHere",
65 | "optional":"false",
66 | "scaleValue":"1",
67 | "tabLabel":"签名 b2391f50-9e5b-41a4-a207-9281bfc81505",
68 | "stampType":"signature"
69 | });
70 |
71 | /////////////// Create initialHere tab ///////////////
72 | // let initialHere = eSignSdk.InitialHere.constructFromObject({
73 | // recipientId: '2',
74 | // documentId: '1',
75 | // pageNumber: '1',
76 | // tabLabel: '初始签名',
77 | // xPosition: '200',
78 | // yPosition: '200',
79 | // });
80 |
81 | /////////////// Create dateSigned tab ///////////////
82 | let dateSigned1 = eSignSdk.DateSigned.constructFromObject({
83 | "documentId":"1",
84 | "height":"0",
85 | "pageNumber":"1",
86 | "recipientId":"21990853",
87 | "tabId":"4d0281fd-9297-48ff-bab5-a8a75af1d4d2",
88 | "tabType":"datesigned",
89 | "templateLocked":"false",
90 | "templateRequired":"false",
91 | "width":"0",
92 | "xPosition":"162",
93 | "yPosition":"233",
94 | "font":"lucidaconsole",
95 | "fontColor":"black",
96 | "fontSize":"size9",
97 | "localePolicy":{
98 | },
99 | "tabLabel":"签名日期 2111c555-7f31-4e0f-be32-76c845e9ae78",
100 | "name":"DateSigned",
101 | "value":""
102 | });
103 |
104 | let dateSigned2 = eSignSdk.DateSigned.constructFromObject({
105 | "documentId":"1",
106 | "height":"0",
107 | "pageNumber":"1",
108 | "recipientId":"56569690",
109 | "tabId":"64f1f246-ede4-4a06-9cee-0791fa7be460",
110 | "tabType":"datesigned",
111 | "templateLocked":"false",
112 | "templateRequired":"false",
113 | "width":"0",
114 | "xPosition":"276",
115 | "yPosition":"233",
116 | "font":"lucidaconsole",
117 | "fontColor":"black",
118 | "fontSize":"size9",
119 | "localePolicy":{
120 | },
121 | "tabLabel":"签名日期 2111c555-7f31-4e0f-be32-76c845e9ae78",
122 | "name":"DateSigned",
123 | "value":""
124 | });
125 |
126 | /////////////// Create attachment tab ///////////////
127 | // let attachmentTab = eSignSdk.SignerAttachment.constructFromObject({
128 | // recipientId: '1',
129 | // documentId: '1',
130 | // pageNumber: '1',
131 | // xPosition: '511',
132 | // yPosition: '626',
133 | // optional: 'true',
134 | // });
135 |
136 | /////////////// Create text fields /////////////////
137 | let address = eSignSdk.Text.constructFromObject({
138 | recipientId: '1',
139 | documentId: '1',
140 | pageNumber: '1',
141 | xPosition: '158',
142 | yPosition: '263',
143 | required: 'false',
144 | tabLabel: '国家',
145 | height: '23',
146 | width: '220',
147 | });
148 |
149 | let city = eSignSdk.Text.constructFromObject({
150 | recipientId: '1',
151 | documentId: '1',
152 | pageNumber: '1',
153 | xPosition: '105',
154 | yPosition: '292',
155 | required: 'true',
156 | tabLabel: '城市',
157 | height: '23',
158 | width: '180',
159 | value: args.contractAmount,
160 | shared: true, // 通过配置share和locked实现预填写字段
161 | locked: true,
162 | });
163 |
164 | /////////////// Create radio tabs ///////////////
165 | let radioGroup1 = eSignSdk.RadioGroup.constructFromObject({
166 | recipientId: '1',
167 | documentId: '1',
168 | groupName: 'radioGroup1',
169 | radios: [
170 | eSignSdk.Radio.constructFromObject({
171 | font: 'helvetica',
172 | fontSize: 'size14',
173 | pageNumber: '1',
174 | value: '1',
175 | xPosition: '73',
176 | yPosition: '386',
177 | required: 'true',
178 | }),
179 | eSignSdk.Radio.constructFromObject({
180 | font: 'helvetica',
181 | fontSize: 'size14',
182 | pageNumber: '1',
183 | value: '2',
184 | xPosition: '143',
185 | yPosition: '386',
186 | required: 'true',
187 | }),
188 | eSignSdk.Radio.constructFromObject({
189 | font: 'helvetica',
190 | fontSize: 'size14',
191 | pageNumber: '1',
192 | value: '3',
193 | xPosition: '222',
194 | yPosition: '386',
195 | required: 'true',
196 | }),
197 | ],
198 | });
199 |
200 | /////////////// Create recipients of the envelope ///////////////
201 | // Create signer recipients to sign the document with the tabs
202 | let signer1 = eSignSdk.Signer.constructFromObject({
203 | email: args.signerEmail,
204 | name: args.signerName,
205 | roleName: 'partyA',
206 | recipientId: '1',
207 | clientUserId: args.signerClientId,
208 | routingOrder: '1',
209 | // emailNotification: { // 如果需要手机号验证,打开这段代码;需要传入countryCode,phoneNumber
210 | // supportedLanguage: 'zh_CN'
211 | // },
212 | // identityVerification: {
213 | // workflowId: args.workflowId,
214 | // steps: null,
215 | // "inputOptions":
216 | // [{
217 | // "name":"phone_number_list",
218 | // "valueType":"PhoneNumberList",
219 | // "phoneNumberList":[
220 | // {
221 | // "countryCode":args.countryCode,
222 | // "code":"1",
223 | // "number":args.phoneNumber
224 | // }
225 | // ]
226 | // }],
227 | // "idCheckConfigurationName":""
228 | // },
229 | tabs: eSignSdk.Tabs.constructFromObject({
230 | // checkboxTabs: [check1, check2, check3, check4],
231 | dateSignedTabs: [dateSigned1],
232 | // emailAddressTabs: [email],
233 | // fullNameTabs: [fullName1, fullName2],
234 | // listTabs: [numEmployees, businessType],
235 | // numberTabs: [loanAmount],
236 | radioGroupTabs: [radioGroup1],
237 | signHereTabs: [signHere1],
238 | // signerAttachmentTabs: [attachmentTab],
239 | textTabs: [
240 | address,
241 | city,
242 | // state,
243 | // zip,
244 | // dateOfBirth,
245 | // homePhone,
246 | // businessPhone,
247 | // businessName,
248 | // loanPurpose,
249 | // explanationBox,
250 | ],
251 | // zipTabs: [zip],
252 | }),
253 | });
254 | let signer2 = eSignSdk.Signer.constructFromObject({
255 | email: '441974767@qq.com',
256 | name: 'smile',
257 | clientUserId: '1000',
258 | recipientId: '2',
259 | routingOrder: '2',
260 | roleName: 'partyB',
261 | tabs: eSignSdk.Tabs.constructFromObject({
262 | dateSignedTabs: [dateSigned2],
263 | // initialHereTabs: [initialHere],
264 | signHereTabs: [signHere2],
265 | }),
266 | });
267 |
268 | // let signer = signer1
269 | // if (args.roleName === 'partyB') {
270 | // signer = signer2
271 | // }
272 |
273 | // Create recipient object
274 | let recipients = eSignSdk.Recipients.constructFromObject({
275 | recipientCount: 2,
276 | signers: [signer1, signer2],
277 | });
278 |
279 | /////////////// Create conditional recipient related objects ///////////////
280 | // Create recipientOption and recipientGroup models
281 | // let signer2a = eSignSdk.RecipientOption.constructFromObject({
282 | // email: args.signerEmail,
283 | // name: smallLenderName,
284 | // roleName: 'Small Lender Signer',
285 | // recipientLabel: 'signer2a',
286 | // });
287 | // let signer2b = eSignSdk.RecipientOption.constructFromObject({
288 | // email: args.signerEmail,
289 | // name: bigLenderName,
290 | // roleName: 'Big Lender Signer',
291 | // recipientLabel: 'signer2b',
292 | // });
293 | // let recipientGroup = eSignSdk.RecipientGroup.constructFromObject({
294 | // groupName: 'Approver',
295 | // groupMessage: 'Members of this group approve a workflow',
296 | // recipients: [signer2a, signer2b],
297 | // });
298 |
299 | // Create conditionalRecipientRuleFilter models
300 | // let filter1 = eSignSdk.ConditionalRecipientRuleFilter.constructFromObject({
301 | // scope: 'tabs',
302 | // recipientId: '1',
303 | // tabId: 'ApprovalTab',
304 | // operator: 'lessThan',
305 | // value: loanBenchmark,
306 | // tabLabel: 'loanAmount',
307 | // tabType: 'number',
308 | // });
309 |
310 | // let filter2 = eSignSdk.ConditionalRecipientRuleFilter.constructFromObject({
311 | // scope: 'tabs',
312 | // recipientId: '1',
313 | // tabId: 'ApprovalTab',
314 | // operator: 'greaterThanEquals',
315 | // value: loanBenchmark,
316 | // tabLabel: 'loanAmount',
317 | // tabType: 'number',
318 | // });
319 |
320 | // Create conditionalRecipientRuleCondition models
321 | // let condition1 =
322 | // eSignSdk.ConditionalRecipientRuleCondition.constructFromObject({
323 | // filters: [filter1],
324 | // order: 1,
325 | // recipientLabel: 'signer2a',
326 | // });
327 | // let condition2 =
328 | // eSignSdk.ConditionalRecipientRuleCondition.constructFromObject({
329 | // filters: [filter2],
330 | // order: 2,
331 | // recipientLabel: 'signer2b',
332 | // });
333 |
334 | // Create conditionalRecipientRule model
335 | // let conditionalRecipient =
336 | // eSignSdk.ConditionalRecipientRule.constructFromObject({
337 | // conditions: [condition1, condition2],
338 | // recipientGroup: recipientGroup,
339 | // recipientId: '2',
340 | // order: 0,
341 | // });
342 |
343 | // Create recipientRouting model
344 | // let recipientRouting = eSignSdk.RecipientRouting.constructFromObject({
345 | // rules: eSignSdk.RecipientRules.constructFromObject({
346 | // conditionalRecipients: [conditionalRecipient],
347 | // }),
348 | // });
349 |
350 | // Create a workflow model
351 | // let workflowStep = eSignSdk.WorkflowStep.constructFromObject({
352 | // action: 'pause_before',
353 | // triggerOnItem: 'routing_order',
354 | // itemId: 2,
355 | // recipientRouting: recipientRouting,
356 | // });
357 | // let workflow = eSignSdk.Workflow.constructFromObject({
358 | // workflowSteps: [workflowStep],
359 | // });
360 |
361 | // Request that the envelope be sent by setting status to "sent".
362 | // To request that the envelope be created as a draft, set status to "created"
363 | return eSignSdk.EnvelopeDefinition.constructFromObject({
364 | emailSubject: text.loanEmailSubject,
365 | brandId: args.brandId,
366 | documents: [doc],
367 | status: args.status,
368 | // workflow: workflow,
369 | recipients: recipients,
370 | enforceSignerVisibility: true,
371 | });
372 | }
373 |
374 | module.exports = {
375 | makeLoanApplicationEnvelope,
376 | };
377 |
--------------------------------------------------------------------------------
/server/docusign/envelopes/makeTrafficTicket.js:
--------------------------------------------------------------------------------
1 | const eSignSdk = require('docusign-esign');
2 | const fs = require('fs');
3 | const text = require('../../assets/public/text.json').trafficTicket;
4 |
5 | /**
6 | * Creates and returns envelope definition for small business loan application.
7 | */
8 | function makeTrafficTicket(args) {
9 | let mitigationClerkName = args.mitigationClerkName;
10 | let contestedClerkName = args.contestedClerkName;
11 | let fineAmount = 237;
12 | let fineName = text.envelope.fineName;
13 | let fineDescription = `$${fineAmount}`;
14 | let currencyMultiplier = 100;
15 |
16 | /////////////// Create documents for envelope ///////////////
17 | // Read and create documents from file in the local directory
18 | let docPdfBytes = fs.readFileSync(args.docFile);
19 | let docb64 = Buffer.from(docPdfBytes).toString('base64');
20 |
21 | let doc = new eSignSdk.Document.constructFromObject({
22 | documentBase64: docb64,
23 | name: text.envelope.ticketDocName, // can be different from actual file name
24 | fileExtension: 'pdf',
25 | documentId: '1',
26 | });
27 |
28 | /////////////// Create initialHere tab ///////////////
29 | let initialHere = eSignSdk.InitialHere.constructFromObject({
30 | recipientId: '1',
31 | documentId: '1',
32 | pageNumber: '1',
33 | tabLabel: 'initialHere',
34 | xPosition: '683',
35 | yPosition: '458',
36 | });
37 |
38 | /////////////// Create name tabs ///////////////
39 | let lastName = eSignSdk.LastName.constructFromObject({
40 | recipientId: '1',
41 | documentId: '1',
42 | pageNumber: '1',
43 | xPosition: '363',
44 | yPosition: '173',
45 | required: 'true',
46 | tabLabel: 'lastName',
47 | height: '12',
48 | width: '60',
49 | });
50 |
51 | let firstName = eSignSdk.FirstName.constructFromObject({
52 | recipientId: '1',
53 | documentId: '1',
54 | pageNumber: '1',
55 | xPosition: '484',
56 | yPosition: '173',
57 | required: 'true',
58 | tabLabel: 'firstName',
59 | height: '12',
60 | width: '60',
61 | });
62 |
63 | /////////////// Create text fields ///////////////
64 | let middleName = eSignSdk.Text.constructFromObject({
65 | recipientId: '1',
66 | documentId: '1',
67 | pageNumber: '1',
68 | xPosition: '588',
69 | yPosition: '173',
70 | required: 'false',
71 | tabLabel: 'middleName',
72 | height: '12',
73 | width: '80',
74 | });
75 |
76 | let address = eSignSdk.Text.constructFromObject({
77 | recipientId: '1',
78 | documentId: '1',
79 | pageNumber: '1',
80 | xPosition: '39',
81 | yPosition: '217',
82 | required: 'false',
83 | tabLabel: 'address',
84 | height: '12',
85 | width: '370',
86 | });
87 |
88 | let dateOfBirth = eSignSdk.Text.constructFromObject({
89 | recipientId: '1',
90 | documentId: '1',
91 | pageNumber: '1',
92 | xPosition: '38',
93 | yPosition: '259',
94 | required: 'true',
95 | tabLabel: 'dateOfBirth',
96 | height: '12',
97 | width: '70',
98 | validationPattern: '^[0-9]{2}/[0-9]{2}/[0-9]{4}$',
99 | validationMessage: 'Date format: MM/DD/YYYY',
100 | });
101 |
102 | let race = eSignSdk.Text.constructFromObject({
103 | recipientId: '1',
104 | documentId: '1',
105 | pageNumber: '1',
106 | xPosition: '120',
107 | yPosition: '259',
108 | required: 'false',
109 | tabLabel: 'race',
110 | height: '12',
111 | width: '65',
112 | });
113 |
114 | let sex = eSignSdk.Text.constructFromObject({
115 | recipientId: '1',
116 | documentId: '1',
117 | pageNumber: '1',
118 | xPosition: '195',
119 | yPosition: '259',
120 | required: 'false',
121 | tabLabel: 'sex',
122 | height: '12',
123 | width: '55',
124 | });
125 |
126 | let height = eSignSdk.Text.constructFromObject({
127 | recipientId: '1',
128 | documentId: '1',
129 | pageNumber: '1',
130 | xPosition: '260',
131 | yPosition: '259',
132 | required: 'false',
133 | tabLabel: 'height',
134 | height: '12',
135 | width: '60',
136 | });
137 |
138 | let eyes = eSignSdk.Text.constructFromObject({
139 | recipientId: '1',
140 | documentId: '1',
141 | pageNumber: '1',
142 | xPosition: '330',
143 | yPosition: '259',
144 | required: 'false',
145 | tabLabel: 'eyes',
146 | height: '12',
147 | width: '60',
148 | });
149 |
150 | let hair = eSignSdk.Text.constructFromObject({
151 | recipientId: '1',
152 | documentId: '1',
153 | pageNumber: '1',
154 | xPosition: '398',
155 | yPosition: '259',
156 | required: 'false',
157 | tabLabel: 'hair',
158 | height: '12',
159 | width: '58',
160 | });
161 |
162 | let homePhone = eSignSdk.Text.constructFromObject({
163 | recipientId: '1',
164 | documentId: '1',
165 | pageNumber: '1',
166 | xPosition: '464',
167 | yPosition: '259',
168 | required: 'true',
169 | tabLabel: 'homePhone',
170 | height: '12',
171 | width: '145',
172 | validationPattern: '^[0-9]{3}-[0-9]{3}-[0-9]{4}$',
173 | validationMessage: 'Phone # format: XXX-XXX-XXXX',
174 | });
175 |
176 | let workPhone = eSignSdk.Text.constructFromObject({
177 | recipientId: '1',
178 | documentId: '1',
179 | pageNumber: '1',
180 | xPosition: '608',
181 | yPosition: '259',
182 | required: 'false',
183 | tabLabel: 'workPhone',
184 | height: '12',
185 | width: '160',
186 | validationPattern: '^[0-9]{3}-[0-9]{3}-[0-9]{4}$',
187 | validationMessage: 'Phone # format: XXX-XXX-XXXX',
188 | });
189 |
190 | let vehYr = eSignSdk.Text.constructFromObject({
191 | recipientId: '1',
192 | documentId: '1',
193 | pageNumber: '1',
194 | xPosition: '260',
195 | yPosition: '304',
196 | required: 'false',
197 | tabLabel: 'vehYr',
198 | height: '12',
199 | width: '60',
200 | });
201 |
202 | let vehMake = eSignSdk.Text.constructFromObject({
203 | recipientId: '1',
204 | documentId: '1',
205 | pageNumber: '1',
206 | xPosition: '329',
207 | yPosition: '304',
208 | required: 'false',
209 | tabLabel: 'vehMake',
210 | height: '12',
211 | width: '135',
212 | });
213 |
214 | let vehModel = eSignSdk.Text.constructFromObject({
215 | recipientId: '1',
216 | documentId: '1',
217 | pageNumber: '1',
218 | xPosition: '465',
219 | yPosition: '304',
220 | required: 'false',
221 | tabLabel: 'vehModel',
222 | height: '12',
223 | width: '145',
224 | });
225 |
226 | let vehStyle = eSignSdk.Text.constructFromObject({
227 | recipientId: '1',
228 | documentId: '1',
229 | pageNumber: '1',
230 | xPosition: '608',
231 | yPosition: '304',
232 | required: 'false',
233 | tabLabel: 'vehStyle',
234 | height: '12',
235 | width: '70',
236 | });
237 |
238 | let vehColor = eSignSdk.Text.constructFromObject({
239 | recipientId: '1',
240 | documentId: '1',
241 | pageNumber: '1',
242 | xPosition: '687',
243 | yPosition: '304',
244 | required: 'false',
245 | tabLabel: 'vehColor',
246 | height: '12',
247 | width: '70',
248 | });
249 |
250 | let officerName = eSignSdk.Text.constructFromObject({
251 | documentId: '1',
252 | pageNumber: '1',
253 | xPosition: '258',
254 | yPosition: '387',
255 | tabLabel: 'officerName',
256 | value: text.names.policeName,
257 | });
258 |
259 | /////////////// Create zip tab ///////////////
260 | let zip = eSignSdk.Zip.constructFromObject({
261 | recipientId: '1',
262 | documentId: '1',
263 | pageNumber: '1',
264 | useDash4: 'false',
265 | xPosition: '687',
266 | yPosition: '218',
267 | required: 'false',
268 | tabLabel: 'zip',
269 | height: '12',
270 | width: '70',
271 | });
272 |
273 | /////////////// Create radio tabs ///////////////
274 | let ticketOptions = eSignSdk.RadioGroup.constructFromObject({
275 | documentId: '1',
276 | groupName: 'ticketOption',
277 | radios: [
278 | eSignSdk.Radio.constructFromObject({
279 | font: 'helvetica',
280 | fontSize: 'size14',
281 | pageNumber: '1',
282 | value: 'Pay',
283 | xPosition: '73',
284 | yPosition: '451',
285 | required: 'true',
286 | }),
287 | eSignSdk.Radio.constructFromObject({
288 | font: 'helvetica',
289 | fontSize: 'size14',
290 | pageNumber: '1',
291 | value: 'Mitigation',
292 | xPosition: '223',
293 | yPosition: '451',
294 | required: 'true',
295 | }),
296 | eSignSdk.Radio.constructFromObject({
297 | font: 'helvetica',
298 | fontSize: 'size14',
299 | pageNumber: '1',
300 | value: 'Contested',
301 | xPosition: '447',
302 | yPosition: '451',
303 | required: 'true',
304 | }),
305 | ],
306 | });
307 |
308 | let radioGroup1 = eSignSdk.RadioGroup.constructFromObject({
309 | recipientId: '1',
310 | documentId: '1',
311 | groupName: 'radioGroup1',
312 | radios: [
313 | eSignSdk.Radio.constructFromObject({
314 | font: 'helvetica',
315 | fontSize: 'size14',
316 | pageNumber: '1',
317 | value: 'Yes',
318 | xPosition: '254',
319 | yPosition: '177',
320 | required: 'true',
321 | }),
322 | eSignSdk.Radio.constructFromObject({
323 | font: 'helvetica',
324 | fontSize: 'size14',
325 | pageNumber: '1',
326 | value: 'No',
327 | xPosition: '288',
328 | yPosition: '177',
329 | required: 'true',
330 | }),
331 | ],
332 | });
333 |
334 | let radioGroup2 = eSignSdk.RadioGroup.constructFromObject({
335 | recipientId: '1',
336 | documentId: '1',
337 | groupName: 'radioGroup2',
338 | radios: [
339 | eSignSdk.Radio.constructFromObject({
340 | font: 'helvetica',
341 | fontSize: 'size14',
342 | pageNumber: '1',
343 | value: 'Yes',
344 | xPosition: '682',
345 | yPosition: '177',
346 | required: 'true',
347 | }),
348 | eSignSdk.Radio.constructFromObject({
349 | font: 'helvetica',
350 | fontSize: 'size14',
351 | pageNumber: '1',
352 | value: 'No',
353 | xPosition: '716',
354 | yPosition: '177',
355 | required: 'true',
356 | }),
357 | ],
358 | });
359 |
360 | /////////////// Create Payment related tabs ///////////////
361 | let fineTab = eSignSdk.FormulaTab.constructFromObject({
362 | documentId: '1',
363 | pageNumber: '1',
364 | font: 'helvetica',
365 | fontSize: 'size11',
366 | xPosition: '155',
367 | yPosition: '490',
368 | tabLabel: 'l1e',
369 | formula: fineAmount,
370 | roundDecimalPlaces: '0',
371 | required: 'true',
372 | locked: 'true',
373 | disableAutoSize: 'false',
374 | conditionalParentLabel: 'ticketOption',
375 | conditionalParentValue: 'Pay',
376 | });
377 |
378 | // Payment line item for passport fee
379 | let paymentLineIteml1 = eSignSdk.PaymentLineItem.constructFromObject({
380 | name: fineName,
381 | description: fineDescription,
382 | amountReference: 'l1e',
383 | });
384 | let paymentDetails = eSignSdk.PaymentDetails.constructFromObject({
385 | gatewayAccountId: args.gatewayAccountId,
386 | currencyCode: 'USD',
387 | gatewayName: args.gatewayName,
388 | gatewayDisplayName: args.gatewayDisplayName,
389 | lineItems: [paymentLineIteml1],
390 | });
391 |
392 | // Hidden formula for the payment itself
393 | let formulaPayment = eSignSdk.FormulaTab.constructFromObject({
394 | tabLabel: 'payment',
395 | formula: `[l1e] * ${currencyMultiplier}`,
396 | roundDecimalPlaces: '0',
397 | paymentDetails: paymentDetails,
398 | hidden: 'true',
399 | required: 'true',
400 | locked: 'true',
401 | documentId: '1',
402 | pageNumber: '1',
403 | xPosition: '0',
404 | yPosition: '0',
405 | });
406 |
407 | /////////////// Create recipients of the envelope ///////////////
408 | // Create signer recipients to sign the document with the tabs
409 | let signer1 = eSignSdk.Signer.constructFromObject({
410 | email: args.signerEmail,
411 | name: args.signerName,
412 | recipientId: '1',
413 | clientUserId: args.signerClientId,
414 | routingOrder: 1,
415 | tabs: eSignSdk.Tabs.constructFromObject({
416 | firstNameTabs: [firstName],
417 | formulaTabs: [fineTab, formulaPayment],
418 | initialHereTabs: [initialHere],
419 | lastNameTabs: [lastName],
420 | radioGroupTabs: [ticketOptions, radioGroup1, radioGroup2],
421 | textTabs: [
422 | address,
423 | dateOfBirth,
424 | eyes,
425 | hair,
426 | height,
427 | homePhone,
428 | middleName,
429 | officerName,
430 | sex,
431 | race,
432 | vehColor,
433 | vehMake,
434 | vehModel,
435 | vehStyle,
436 | vehYr,
437 | workPhone,
438 | ],
439 | zipTabs: [zip],
440 | }),
441 | });
442 | let certifiedDeliveryRecipient =
443 | eSignSdk.CertifiedDelivery.constructFromObject({
444 | email: 'placeholder@example.com',
445 | name: 'Approver',
446 | recipientId: '2',
447 | routingOrder: 2,
448 | });
449 |
450 | // Create recipient object
451 | let recipients = eSignSdk.Recipients.constructFromObject({
452 | signers: [signer1],
453 | certifiedDeliveries: [certifiedDeliveryRecipient],
454 | });
455 |
456 | /////////////// Create conditional recipient related objects ///////////////
457 | // Create recipientOption and recipientGroup models
458 | let cdRecipientA = eSignSdk.RecipientOption.constructFromObject({
459 | email: args.signerEmail,
460 | name: mitigationClerkName,
461 | roleName: 'Mitigation Clerk',
462 | recipientLabel: 'cdRecipientA',
463 | });
464 | let cdRecipientB = eSignSdk.RecipientOption.constructFromObject({
465 | email: args.signerEmail,
466 | name: contestedClerkName,
467 | roleName: 'Contested Clerk',
468 | recipientLabel: 'cdRecipientB',
469 | });
470 | let recipientGroup = eSignSdk.RecipientGroup.constructFromObject({
471 | groupName: 'Court Clerks',
472 | groupMessage: 'Members of this group approve a workflow',
473 | recipients: [cdRecipientA, cdRecipientB],
474 | });
475 |
476 | // Create conditionalRecipientRuleFilter models
477 | let filter1 = eSignSdk.ConditionalRecipientRuleFilter.constructFromObject({
478 | scope: 'tabs',
479 | recipientId: '1',
480 | tabId: 'ApprovalTab',
481 | operator: 'equals',
482 | value: 'Mitigation',
483 | tabLabel: 'ticketOption',
484 | tabType: 'radioGroup',
485 | });
486 | let filter2 = eSignSdk.ConditionalRecipientRuleFilter.constructFromObject({
487 | scope: 'tabs',
488 | recipientId: '1',
489 | tabId: 'ApprovalTab',
490 | operator: 'equals',
491 | value: 'Contested',
492 | tabLabel: 'ticketOption',
493 | tabType: 'radioGroup',
494 | });
495 |
496 | // Create conditionalRecipientRuleCondition models
497 | let condition1 =
498 | eSignSdk.ConditionalRecipientRuleCondition.constructFromObject({
499 | filters: [filter1],
500 | order: 1,
501 | recipientLabel: 'cdRecipientA',
502 | });
503 | let condition2 =
504 | eSignSdk.ConditionalRecipientRuleCondition.constructFromObject({
505 | filters: [filter2],
506 | order: 2,
507 | recipientLabel: 'cdRecipientB',
508 | });
509 |
510 | // Create conditionalRecipientRule model
511 | let conditionalRecipient =
512 | eSignSdk.ConditionalRecipientRule.constructFromObject({
513 | conditions: [condition1, condition2],
514 | recipientGroup: recipientGroup,
515 | recipientId: '2',
516 | order: 0,
517 | });
518 |
519 | // Create recipientRouting model
520 | let recipientRouting = eSignSdk.RecipientRouting.constructFromObject({
521 | rules: eSignSdk.RecipientRules.constructFromObject({
522 | conditionalRecipients: [conditionalRecipient],
523 | }),
524 | });
525 |
526 | // Create a workflow model
527 | let workflowStep = eSignSdk.WorkflowStep.constructFromObject({
528 | action: 'pause_before',
529 | triggerOnItem: 'routing_order',
530 | itemId: 2,
531 | recipientRouting: recipientRouting,
532 | });
533 | let workflow = eSignSdk.Workflow.constructFromObject({
534 | workflowSteps: [workflowStep],
535 | });
536 |
537 | // Request that the envelope be sent by setting status to "sent".
538 | // To request that the envelope be created as a draft, set status to "created"
539 | return eSignSdk.EnvelopeDefinition.constructFromObject({
540 | emailSubject: text.envelope.ticketEmailSubject,
541 | documents: [doc],
542 | status: args.status,
543 | workflow: workflow,
544 | recipients: recipients,
545 | });
546 | }
547 | module.exports = {
548 | makeTrafficTicket,
549 | };
550 |
--------------------------------------------------------------------------------