├── .dockerignore
├── .env.example
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── assets
└── quickstart.jpeg
├── docker-compose.yml
├── frontend
├── .gitignore
├── .npmrc
├── .nvmrc
├── Dockerfile
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ ├── App.module.scss
│ ├── App.tsx
│ ├── Components
│ │ ├── Endpoint
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── Error
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── Headers
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── Link
│ │ │ └── index.tsx
│ │ ├── ProductTypes
│ │ │ ├── Items.tsx
│ │ │ ├── ProductTypesContainer
│ │ │ │ ├── index.module.scss
│ │ │ │ └── index.tsx
│ │ │ └── Products.tsx
│ │ └── Table
│ │ │ ├── Identity.module.scss
│ │ │ ├── Identity.tsx
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ ├── Context
│ │ └── index.tsx
│ ├── dataUtilities.ts
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── setupProxy.js
│ └── setupTests.ts
└── tsconfig.json
├── go
├── .env
├── .env.example
├── Dockerfile
├── go.mod
├── go.sum
├── server.go
└── start.sh
├── java
├── .env
├── .env.example
├── .gitignore
├── Dockerfile
├── config.yml
├── pom.xml
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── plaid
│ │ └── quickstart
│ │ ├── QuickstartApplication.java
│ │ ├── QuickstartConfiguration.java
│ │ └── resources
│ │ ├── AccessTokenResource.java
│ │ ├── AccountsResource.java
│ │ ├── AssetsResource.java
│ │ ├── AuthResource.java
│ │ ├── BalanceResource.java
│ │ ├── CraResource.java
│ │ ├── HoldingsResource.java
│ │ ├── IdentityResource.java
│ │ ├── InfoResource.java
│ │ ├── InvestmentTransactionsResource.java
│ │ ├── ItemResource.java
│ │ ├── LinkTokenResource.java
│ │ ├── LinkTokenWithPaymentResource.java
│ │ ├── PaymentInitiationResource.java
│ │ ├── PublicTokenResource.java
│ │ ├── SignalResource.java
│ │ ├── StatementsResource.java
│ │ ├── TransactionsResource.java
│ │ ├── TransferAuthorizeResource.java
│ │ ├── TransferCreateResource.java
│ │ └── UserTokenResource.java
└── start.sh
├── node
├── .dockerignore
├── .env
├── .env.example
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc
├── Dockerfile
├── index.js
├── package-lock.json
├── package.json
└── start.sh
├── python
├── .env
├── .env.example
├── Dockerfile
├── requirements.txt
├── server.py
└── start.sh
└── ruby
├── .env
├── .env.example
├── .gitignore
├── .ruby-version
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── app.rb
└── start.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/team/keys
2 | PLAID_CLIENT_ID=
3 | PLAID_SECRET=
4 |
5 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
6 | # Use 'production' to use real data
7 | # NOTE: Some major US institutions (including Chase, Wells Fargo, Bank of America) won't work unless you have been approved for full production.
8 | # To test these institutions with live data, get full production approval first at https://dashboard.plaid.com/overview/production
9 | # Once approved, set your environment to 'production' to test.
10 | # NOTE: To use Production, you must set a use case for Link.
11 | # You can do this in the Dashboard under Link -> Link Customization -> Data Transparency:
12 | # https://dashboard.plaid.com/link/data-transparency-v5
13 | PLAID_ENV=sandbox
14 |
15 | # PLAID_PRODUCTS is a comma-separated list of products to use when
16 | # initializing Link, e.g. PLAID_PRODUCTS=auth,transactions.
17 | # see https://plaid.com/docs/api/link/#link-token-create-request-products for a complete list.
18 | # Only institutions that support ALL listed products will work.
19 | # If you don't see the institution you want in Link, or get a "Connectivity not supported" error,
20 | # Remove any products you aren't using.
21 | # NOTE: The Identity Verification (IDV) and Income APIs have separate Quickstart apps.
22 | # For IDV, use https://github.com/plaid/idv-quickstart
23 | # For Income, use https://github.com/plaid/income-sample
24 | # Important:
25 | # When moving to Production, make sure to update this list with only the products
26 | # you plan to use. Otherwise, you may be billed for unneeded products.
27 | PLAID_PRODUCTS=auth,transactions
28 |
29 | # PLAID_COUNTRY_CODES is a comma-separated list of countries to use when
30 | # initializing Link, e.g. PLAID_COUNTRY_CODES=US,CA.
31 | # Institutions from all listed countries will be shown. If Link is launched with multiple country codes,
32 | # only products that you are enabled for in all countries will be used by Link.
33 | # See https://plaid.com/docs/api/link/#link-token-create-request-country-codes for a complete list
34 | PLAID_COUNTRY_CODES=US,CA
35 |
36 | # PLAID_REDIRECT_URI is optional for this Quickstart application.
37 | # If you're not sure if you need to use this field, you can leave it blank
38 | #
39 | # If using this field on Sandbox, set PLAID_REDIRECT_URI to http://localhost:3000/ (no quote characters)
40 | # The OAuth redirect flow requires an endpoint on the developer's website
41 | # that the bank website should redirect to. You will need to configure
42 | # this redirect URI for your client ID through the Plaid developer dashboard
43 | # at https://dashboard.plaid.com/team/api.
44 | # For development or production, you will need to use an https:// url
45 | # Instructions to create a self-signed certificate for localhost can be found at https://github.com/plaid/quickstart/blob/master/README.md#testing-oauth
46 | PLAID_REDIRECT_URI=
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.swp
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # nyc test coverage
19 | .nyc_output
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Compiled binary addons (http://nodejs.org/api/addons.html)
28 | build/Release
29 |
30 | # Dependency directories
31 | node_modules
32 | jspm_packages
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 |
40 | # go
41 | go/quickstart
42 |
43 | # macOS
44 | **/.DS_Store
45 |
46 | # docker-compose
47 | docker-compose.local*.yml
48 |
49 | *.iml
50 | .env
51 | .idea
52 |
53 | #vscode
54 | .vscode
55 |
56 | #Python virtual nv
57 | python/venv/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Plaid
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | DOCKER_COMPOSE := docker compose
2 | DOCKER_COMPOSE_YML := --file docker-compose.yml
3 | ifneq ("$(wildcard docker-compose.local.yml)","")
4 | DOCKER_COMPOSE_YML += --file docker-compose.local.yml
5 | endif
6 |
7 | language := node
8 | SUCCESS_MESSAGE := "✅ $(language) quickstart is running on http://localhost:3000"
9 |
10 | .PHONY: up
11 | up:
12 | REACT_APP_API_HOST=http://$(language):8000 \
13 | $(DOCKER_COMPOSE) \
14 | $(DOCKER_COMPOSE_YML) \
15 | $@ --build --detach --remove-orphans \
16 | $(language)
17 | @echo $(SUCCESS_MESSAGE)
18 |
19 | .PHONY: logs
20 | logs:
21 | $(DOCKER_COMPOSE) \
22 | $@ --follow \
23 | $(language) frontend
24 |
25 | .PHONY: stop build
26 | stop build:
27 | $(DOCKER_COMPOSE) \
28 | $(DOCKER_COMPOSE_YML) \
29 | $@ \
30 | $(language) frontend
31 |
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plaid quickstart
2 |
3 | This repository accompanies Plaid's [**quickstart guide**][quickstart].
4 |
5 | Here you'll find full example integration apps using our [**client libraries**][libraries].
6 |
7 | This Quickstart is designed to show as many products and configurations as possible, including all five officially supported client libraries and multiple Plaid APIs, against a React frontend.
8 |
9 | If you prefer a non-React frontend platform, or a more minimal backend in one language with one endpoint, see the [Tiny Quickstart](https://github.com/plaid/tiny-quickstart), which shows a simpler backend and is available for JavaScript, Next.js, React, and React Native frontends.
10 |
11 | For Identity Verification, see the [Identity Verification Quickstart](https://github.com/plaid/idv-quickstart).
12 |
13 | For Income, see the [Income sample app](https://github.com/plaid/income-sample).
14 |
15 | For a more in-depth Transfer Quickstart, see the [Transfer Quickstart](https://github.com/plaid/transfer-quickstart) (Node only).
16 |
17 | 
18 |
19 | ## Table of contents
20 |
21 |
22 |
23 | - [1. Clone the repository](#1-clone-the-repository)
24 | - [Special instructions for Windows](#special-instructions-for-windows)
25 | - [2. Set up your environment variables](#2-set-up-your-environment-variables)
26 | - [3. Run the quickstart](#3-run-the-quickstart)
27 | - [Run without Docker](#run-without-docker)
28 | - [Pre-requisites](#pre-requisites)
29 | - [1. Running the backend](#1-running-the-backend)
30 | - [Node](#node)
31 | - [Python](#python)
32 | - [Ruby](#ruby)
33 | - [Go](#go)
34 | - [Java](#java)
35 | - [.NET](#net) (community support only)
36 | - [2. Running the frontend](#2-running-the-frontend)
37 | - [Run with Docker](#run-with-docker)
38 | - [Pre-requisites](#pre-requisites-1)
39 | - [Running](#running-1)
40 | - [Start the container](#start-the-container)
41 | - [View the logs](#view-the-logs)
42 | - [Stop the container](#stop-the-container)
43 | - [Test credentials](#test-credentials)
44 | - [Troubleshooting](#troubleshooting)
45 | - [Testing OAuth](#testing-oauth)
46 |
47 |
48 |
49 | ## 1. Clone the repository
50 |
51 | Using https:
52 |
53 | ```bash
54 | git clone https://github.com/plaid/quickstart
55 | cd quickstart
56 | ```
57 |
58 | Alternatively, if you use ssh:
59 |
60 | ```bash
61 | git clone git@github.com:plaid/quickstart.git
62 | cd quickstart
63 | ```
64 |
65 | #### Special instructions for Windows
66 |
67 | Note - because this repository makes use of symbolic links, to run this on a Windows machine, make sure you have checked the "enable symbolic links" box when you download Git to your local machine. Then you can run the above commands to clone the quickstart. Otherwise, you may open your Git Bash terminal as an administrator and use the following command when cloning the project
68 |
69 | ```bash
70 | git clone -c core.symlinks=true https://github.com/plaid/quickstart
71 | ```
72 |
73 | ## 2. Set up your environment variables
74 |
75 | ```bash
76 | cp .env.example .env
77 | ```
78 |
79 | Copy `.env.example` to a new file called `.env` and fill out the environment variables inside. At
80 | minimum `PLAID_CLIENT_ID` and `PLAID_SECRET` must be filled out. Get your Client ID and secrets from
81 | the dashboard: [https://dashboard.plaid.com/developers/keys](https://dashboard.plaid.com/developers/keys)
82 |
83 | > NOTE: `.env` files are a convenient local development tool. Never run a production application
84 | > using an environment file with secrets in it.
85 |
86 | ## 3. Run the Quickstart
87 |
88 | There are two ways to run the various language quickstarts in this repository. You can choose to run the
89 | code directly or you can run it in Docker. If you would like to run the code via Docker, skip to the
90 | [Run with Docker](#run-with-docker) section.
91 |
92 | ### Run without Docker
93 |
94 | #### Pre-requisites
95 |
96 | - The language you intend to use is installed on your machine and available at your command line.
97 | This repo should generally work with active LTS versions of each language such as node >= 14,
98 | python >= 3.8, ruby >= 2.6, etc.
99 | - Your environment variables populated in `.env`
100 | - [npm](https://www.npmjs.com/get-npm)
101 | - If using Windows, a command line utility capable of running basic Unix shell commands
102 |
103 | #### 1. Running the backend
104 |
105 | Once started with one of the commands below, the quickstart will be running on http://localhost:8000 for the backend. Enter the additional commands in step 2 to run the frontend which will run on http://localhost:3000.
106 |
107 | ##### Node
108 |
109 | ```bash
110 | $ cd ./node
111 | $ npm install
112 | $ ./start.sh
113 | ```
114 |
115 | ##### Python
116 |
117 | **:warning: As `python2` has reached its end of life, only `python3` is supported.**
118 |
119 | ```bash
120 | cd ./python
121 |
122 | # If you use virtualenv
123 | # virtualenv venv
124 | # source venv/bin/activate
125 |
126 | pip3 install -r requirements.txt
127 | ./start.sh
128 | ```
129 |
130 | If you get this error message:
131 |
132 | ```txt
133 | ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749)
134 | ```
135 |
136 | You may need to run the following command in your terminal for your particular version of python in order to install SSL certificates:
137 |
138 | ```bash
139 | # examples:
140 | open /Applications/Python\ 3.9/Install\ Certificates.command
141 | # or
142 | open /Applications/Python\ 3.6/Install\ Certificates.command
143 | ```
144 |
145 | ##### Ruby
146 |
147 | ```bash
148 | cd ./ruby
149 | bundle
150 | ./start.sh
151 | ```
152 |
153 | ##### Go
154 |
155 | ```bash
156 | cd ./go
157 | go build
158 | ./start.sh
159 | ```
160 |
161 | ##### Java
162 |
163 | ```bash
164 | cd ./java
165 | mvn clean package
166 | ./start.sh
167 | ```
168 |
169 | ##### .NET
170 |
171 | A community-supported implementation of the Plaid Quickstart using the [Going.Plaid](https://github.com/viceroypenguin/Going.Plaid) client library can be found at [PlaidQuickstartBlazor](https://github.com/jcoliz/PlaidQuickstartBlazor). Note that Plaid does not provide first-party support for .NET client libraries and that this Quickstart and client library are not created, reviewed, or supported by Plaid.
172 |
173 | #### 2. Running the frontend
174 |
175 | ```bash
176 | cd ./frontend
177 | npm ci
178 | npm start
179 | ```
180 |
181 | ### Run with Docker
182 |
183 | #### Pre-requisites
184 |
185 | - `make` available at your command line
186 | - Docker installed and running on your machine: https://docs.docker.com/get-docker/
187 | - Your environment variables populated in `.env`
188 | - If using Windows, a working Linux installation on Windows 10. If you are using Windows and do not already have WSL or Cygwin configured, we recommend [running without Docker](#run-without-docker).
189 |
190 | #### Running
191 |
192 | There are three basic `make` commands available
193 |
194 | - `up`: builds and starts the container
195 | - `logs`: tails logs
196 | - `stop`: stops the container
197 |
198 | Each of these should be used with a `language` argument, which is one of `node`, `python`, `ruby`,
199 | `java`, or `go`. If unspecified, the default is `node`.
200 |
201 | ##### Start the container
202 |
203 | ```bash
204 | make up language=node
205 | ```
206 |
207 | The quickstart backend is now running on http://localhost:8000 and frontend on http://localhost:3000.
208 |
209 | If you make changes to one of the server files such as `index.js`, `server.go`, etc, or to the
210 | `.env` file, simply run `make up language=node` again to rebuild and restart the container.
211 |
212 | If you experience a Docker connection error when running the command above, try the following:
213 |
214 | - Make sure Docker is running
215 | - Try running the command prefixed with `sudo`
216 |
217 | ##### View the logs
218 |
219 | ```bash
220 | make logs language=node
221 | ```
222 |
223 | ##### Stop the container
224 |
225 | ```bash
226 | make stop language=node
227 | ```
228 |
229 | ## Test credentials
230 |
231 | In Sandbox, you can log in to any supported institution using `user_good` as the username and `pass_good` as the password. If prompted to enter a 2-factor authentication code, enter `1234`. In Production, use real-life credentials.
232 |
233 | ### Transactions test credentials
234 | For Transactions, you will get the most realistic results using `user_transactions_dynamic` as the username, and any non-blank string as the password. For more details on the special capabilities of this test user, see the [docs](https://plaid.com/docs/transactions/transactions-data/#testing-pending-and-posted-transactions).
235 |
236 | ### Credit test credentials
237 | For credit and underwriting products like Assets and Statements, you will get the most realistic results using one of the [credit and underwriting tests credentials](https://plaid.com/docs/sandbox/test-credentials/#credit-and-income-testing-credentials), like `user_bank_income` / `{}`.
238 |
239 | ## Troubleshooting
240 |
241 | ### Link fails in Production with "something went wrong" / `INVALID_SERVER_ERROR` but works in Sandbox
242 |
243 | If Link works in Sandbox but fails in Production, the error is most likely one of the following:
244 | 1) You need to set a use case for Link, which you can do in the Plaid Dashboard under [Link -> Customization -> Data Transparency Messaging](https://dashboard.plaid.com/link/data-transparency-v5).
245 | 2) You don't yet have OAuth access for the institution you selected. This is especially common if the institution is Chase or Charles Schwab, which have longer OAuth registration turnarounds. To check your OAuth registration status and see if you have any required action items, see the [US OAuth Institutions page](https://dashboard.plaid.com/settings/compliance/us-oauth-institutions) in the Dashboard.
246 |
247 | ### Can't get a link token, or API calls are 400ing
248 |
249 | View the server logs to see the associated error message with detailed troubleshooting instructions. If you can't view logs locally, view them via the [Dashboard activity logs](https://dashboard.plaid.com/activity/logs).
250 |
251 | ### Works only when `PLAID_REDIRECT_URI` is not specified
252 | Make sure to add the redirect URI to the Allowed Redirect URIs list in the [Plaid Dashboard](https://dashboard.plaid.com/team/api).
253 |
254 | ### "Connectivity not supported"
255 |
256 | If you get a "Connectivity not supported" error after selecting a financial institution in Link, you probably specified some products in your .env file that the target financial institution doesn't support. Remove the unsupported products and try again.
257 |
258 | ### "You need to update your app" or "institution not supported"
259 |
260 | If you get a "You need to update your app" or "institution not supported" error after selecting a financial institution in Link, you're probably running the Quickstart in Production and attempting to link an institution, such as Chase or Wells Fargo, that requires an OAuth-based connection. In order to make OAuth connections to US-based institutions in Production, you must have full Production access approval, and certain institutions may also require additional approvals before you can be enabled. To use this institution, [apply for full Production access](https://dashboard.plaid.com/overview/production) and see the [OAuth insitutions page](https://dashboard.plaid.com/team/oauth-institutions) for any other required steps and to track your OAuth enablement status.
261 |
262 | ### "oauth uri does not contain a valid oauth_state_id query parameter"
263 |
264 | If you get the console error "oauth uri does not contain a valid oauth_state_id query parameter", you are attempting to initialize Link with a redirect uri when it is not necessary to do so. The `receivedRedirectUri` should not be set when initializing Link for the first time. It is used when initializing Link for the second time, after returning from the OAuth redirect.
265 |
266 | ## Testing OAuth
267 |
268 | Some institutions require an OAuth redirect
269 | authentication flow, where the end user is redirected to the bank’s website or mobile app to
270 | authenticate.
271 |
272 | To test the OAuth flow in Sandbox, select any institution that uses an OAuth connection with Plaid (a partial list can be found on the [Dashboard OAuth Institutions page](https://dashboard.plaid.com/team/oauth-institutions)), or choose 'Platypus OAuth Bank' from the list of financial institutions in Plaid Link.
273 |
274 | ### Testing OAuth with a redirect URI (optional)
275 |
276 | To test the OAuth flow in Sandbox with a [redirect URI](https://www.plaid.com/docs/link/oauth/#create-and-register-a-redirect-uri), you should set `PLAID_REDIRECT_URI=http://localhost:3000/` in `.env`. You will also need to register this localhost redirect URI in the
277 | [Plaid dashboard under Developers > API > Allowed redirect URIs][dashboard-api-section]. It is not required to configure a redirect URI in the .env file to use OAuth with the Quickstart.
278 |
279 | #### Instructions for using https with localhost
280 |
281 | If you want to test OAuth in development with a redirect URI, you need to use https and set `PLAID_REDIRECT_URI=https://localhost:3000/` in `.env`. In order to run your localhost on https, you will need to create a self-signed certificate and add it to the frontend root folder. You can use the following instructions to do this. Note that self-signed certificates should be used for testing purposes only, never for actual deployments.
282 |
283 | In your terminal, change to the frontend folder:
284 |
285 | ```bash
286 | cd frontend
287 | ```
288 |
289 | Use homebrew to install mkcert:
290 |
291 | ```bash
292 | brew install mkcert
293 | ```
294 |
295 | Then create your certificate for localhost:
296 |
297 | ```bash
298 | mkcert -install
299 | mkcert localhost
300 | ```
301 |
302 | This will create a certificate file localhost.pem and a key file localhost-key.pem inside your client folder.
303 |
304 | Then in the package.json file in the frontend folder, replace this line on line 28
305 |
306 | ```bash
307 | "start": "react-scripts start",
308 | ```
309 |
310 | with this line instead:
311 |
312 | ```bash
313 | "start": "HTTPS=true SSL_CRT_FILE=localhost.pem SSL_KEY_FILE=localhost-key.pem react-scripts start",
314 | ```
315 |
316 | After starting up the Quickstart, you can now view it at https://localhost:3000. If you are on Windows, you
317 | may still get an invalid certificate warning on your browser. If so, click on "advanced" and proceed. Also on Windows, the frontend may still try to load http://localhost:3000 and you may have to access https://localhost:3000 manually.
318 |
319 | [quickstart]: https://plaid.com/docs/quickstart
320 | [libraries]: https://plaid.com/docs/api/libraries
321 | [payment-initiation]: https://plaid.com/docs/payment-initiation/
322 | [node-example]: /node
323 | [ruby-example]: /ruby
324 | [python-example]: /python
325 | [java-example]: /java
326 | [go-example]: /go
327 | [docker]: https://www.docker.com
328 | [dashboard-api-section]: https://dashboard.plaid.com/developers/api
329 | [contact-sales]: https://plaid.com/contact
330 |
--------------------------------------------------------------------------------
/assets/quickstart.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plaid/quickstart/ec8cfa1702506f6e98d5c2973be8b23c4fc72964/assets/quickstart.jpeg
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 | x-environment:
3 | &QUICKSTART_ENVIRONMENT # These are read from .env file. The values in the .env file maybe overriden by shell envvars
4 | PLAID_CLIENT_ID: ${PLAID_CLIENT_ID}
5 | PLAID_SECRET: ${PLAID_SECRET}
6 | PLAID_PRODUCTS: ${PLAID_PRODUCTS}
7 | PLAID_COUNTRY_CODES: ${PLAID_COUNTRY_CODES}
8 | PLAID_REDIRECT_URI: ${PLAID_REDIRECT_URI}
9 | PLAID_ENV: ${PLAID_ENV}
10 | services:
11 | go:
12 | networks:
13 | - "quickstart"
14 | depends_on:
15 | - "frontend"
16 | build:
17 | context: .
18 | dockerfile: ./go/Dockerfile
19 | ports: ["8000:8000"]
20 | environment:
21 | <<: *QUICKSTART_ENVIRONMENT
22 | java:
23 | networks:
24 | - "quickstart"
25 | depends_on:
26 | - "frontend"
27 | build:
28 | context: .
29 | dockerfile: ./java/Dockerfile
30 | ports: ["8000:8000"]
31 | environment:
32 | <<: *QUICKSTART_ENVIRONMENT
33 | node:
34 | networks:
35 | - "quickstart"
36 | depends_on:
37 | - "frontend"
38 | build:
39 | context: .
40 | dockerfile: ./node/Dockerfile
41 | ports: ["8000:8000"]
42 | environment:
43 | <<: *QUICKSTART_ENVIRONMENT
44 | python:
45 | networks:
46 | - "quickstart"
47 | depends_on:
48 | - "frontend"
49 | build:
50 | context: .
51 | dockerfile: ./python/Dockerfile
52 | ports: ["8000:8000"]
53 | environment:
54 | <<: *QUICKSTART_ENVIRONMENT
55 | ruby:
56 | networks:
57 | - "quickstart"
58 | depends_on:
59 | - "frontend"
60 | build:
61 | context: .
62 | dockerfile: ./ruby/Dockerfile
63 | ports: ["8000:8000"]
64 | environment:
65 | <<: *QUICKSTART_ENVIRONMENT
66 | frontend:
67 | environment:
68 | - REACT_APP_API_HOST
69 | networks:
70 | - "quickstart"
71 | build:
72 | context: .
73 | dockerfile: ./frontend/Dockerfile
74 | ports: ["3000:3000"]
75 | networks:
76 | quickstart:
77 | name: quickstart
78 |
--------------------------------------------------------------------------------
/frontend/.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 | .eslintcache
25 |
--------------------------------------------------------------------------------
/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.com
2 | engine-strict=true
3 |
--------------------------------------------------------------------------------
/frontend/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.1
2 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # pull official base image
2 | FROM node:16-alpine
3 |
4 | # set working directory
5 | WORKDIR /app
6 |
7 | # add `/app/node_modules/.bin` to $PATH
8 | ENV PATH /app/node_modules/.bin:$PATH
9 |
10 | # install app dependencies
11 | COPY ./frontend/.npmrc ./
12 | COPY ./frontend/package*.json ./
13 | RUN npm install
14 |
15 | # add app
16 | COPY ./frontend ./
17 |
18 | # start app
19 | CMD ["npm", "start"]
20 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plaid_react_quickstart",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "@testing-library/jest-dom": "^5.11.4",
6 | "@testing-library/react": "^11.1.0",
7 | "@testing-library/user-event": "^12.1.10",
8 | "@types/jest": "^26.0.15",
9 | "@types/node": "^12.0.0",
10 | "@types/react": "^16.9.53",
11 | "@types/react-dom": "^16.9.8",
12 | "ajv": "^8.11.2",
13 | "ajv-keywords": "^5.1.0",
14 | "classnames": "^2.2.6",
15 | "immer": "^9.0.6",
16 | "plaid": "^26.0.0",
17 | "plaid-threads": "^11.2.3",
18 | "react": "^16.14.0",
19 | "react-dev-utils": "^12.0.0",
20 | "react-dom": "^16.14.0",
21 | "react-plaid-link": "^3.2.1",
22 | "sass": "^1.32.8",
23 | "sass-loader": "^10.1.1",
24 | "typescript": "^4.0.3",
25 | "web-vitals": "^0.2.4"
26 | },
27 | "devDependencies": {
28 | "@types/classnames": "^2.2.11",
29 | "react-scripts": "^5.0.1"
30 | },
31 | "peerDependencies": {
32 | "chalk": "^4.1.2"
33 | },
34 | "scripts": {
35 | "start": "react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject",
39 | "preinstall": "npx npm-force-resolutions"
40 | },
41 | "eslintConfig": {
42 | "extends": [
43 | "react-app",
44 | "react-app/jest"
45 | ]
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | },
59 | "overrides": {
60 | "svgo": {
61 | "nth-check": ">=2.0.2"
62 | },
63 | "react-scripts": {
64 | "postcss": ">=8.4.31"
65 | }
66 | },
67 | "engines": {
68 | "node": ">=14.0.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Plaid Quickstart
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/App.module.scss:
--------------------------------------------------------------------------------
1 | $threads-font-path: "~plaid-threads/fonts";
2 | @import "~plaid-threads/scss/typography";
3 | @import "~plaid-threads/scss/variables";
4 |
5 | * {
6 | box-sizing: border-box;
7 | }
8 |
9 | .container {
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | min-width: 70 * $unit;
14 | max-width: 120 * $unit;
15 | margin: 0 auto;
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext, useCallback } from "react";
2 |
3 | import Header from "./Components/Headers";
4 | import Products from "./Components/ProductTypes/Products";
5 | import Items from "./Components/ProductTypes/Items";
6 | import Context from "./Context";
7 |
8 | import styles from "./App.module.scss";
9 | import { CraCheckReportProduct } from "plaid";
10 |
11 | const App = () => {
12 | const { linkSuccess, isPaymentInitiation, itemId, dispatch } =
13 | useContext(Context);
14 |
15 | const getInfo = useCallback(async () => {
16 | const response = await fetch("/api/info", { method: "POST" });
17 | if (!response.ok) {
18 | dispatch({ type: "SET_STATE", state: { backend: false } });
19 | return { paymentInitiation: false };
20 | }
21 | const data = await response.json();
22 | const paymentInitiation: boolean =
23 | data.products.includes("payment_initiation");
24 | const craEnumValues = Object.values(CraCheckReportProduct);
25 | const isUserTokenFlow: boolean = data.products.some(
26 | (product: CraCheckReportProduct) => craEnumValues.includes(product)
27 | );
28 | const isCraProductsExclusively: boolean = data.products.every(
29 | (product: CraCheckReportProduct) => craEnumValues.includes(product)
30 | );
31 | dispatch({
32 | type: "SET_STATE",
33 | state: {
34 | products: data.products,
35 | isPaymentInitiation: paymentInitiation,
36 | isCraProductsExclusively: isCraProductsExclusively,
37 | isUserTokenFlow: isUserTokenFlow,
38 | },
39 | });
40 | return { paymentInitiation, isUserTokenFlow };
41 | }, [dispatch]);
42 |
43 | const generateUserToken = useCallback(async () => {
44 | const response = await fetch("api/create_user_token", { method: "POST" });
45 | if (!response.ok) {
46 | dispatch({ type: "SET_STATE", state: { userToken: null } });
47 | return;
48 | }
49 | const data = await response.json();
50 | if (data) {
51 | if (data.error != null) {
52 | dispatch({
53 | type: "SET_STATE",
54 | state: {
55 | linkToken: null,
56 | linkTokenError: data.error,
57 | },
58 | });
59 | return;
60 | }
61 | dispatch({ type: "SET_STATE", state: { userToken: data.user_token } });
62 | return data.user_token;
63 | }
64 | }, [dispatch]);
65 |
66 | const generateToken = useCallback(
67 | async (isPaymentInitiation) => {
68 | // Link tokens for 'payment_initiation' use a different creation flow in your backend.
69 | const path = isPaymentInitiation
70 | ? "/api/create_link_token_for_payment"
71 | : "/api/create_link_token";
72 | const response = await fetch(path, {
73 | method: "POST",
74 | });
75 | if (!response.ok) {
76 | dispatch({ type: "SET_STATE", state: { linkToken: null } });
77 | return;
78 | }
79 | const data = await response.json();
80 | if (data) {
81 | if (data.error != null) {
82 | dispatch({
83 | type: "SET_STATE",
84 | state: {
85 | linkToken: null,
86 | linkTokenError: data.error,
87 | },
88 | });
89 | return;
90 | }
91 | dispatch({ type: "SET_STATE", state: { linkToken: data.link_token } });
92 | }
93 | // Save the link_token to be used later in the Oauth flow.
94 | localStorage.setItem("link_token", data.link_token);
95 | },
96 | [dispatch]
97 | );
98 |
99 | useEffect(() => {
100 | const init = async () => {
101 | const { paymentInitiation, isUserTokenFlow } = await getInfo(); // used to determine which path to take when generating token
102 | // do not generate a new token for OAuth redirect; instead
103 | // setLinkToken from localStorage
104 | if (window.location.href.includes("?oauth_state_id=")) {
105 | dispatch({
106 | type: "SET_STATE",
107 | state: {
108 | linkToken: localStorage.getItem("link_token"),
109 | },
110 | });
111 | return;
112 | }
113 |
114 | if (isUserTokenFlow) {
115 | await generateUserToken();
116 | }
117 | generateToken(paymentInitiation);
118 | };
119 | init();
120 | }, [dispatch, generateToken, generateUserToken, getInfo]);
121 |
122 | return (
123 |
124 |
125 |
126 | {linkSuccess && (
127 | <>
128 |
129 | {!isPaymentInitiation && itemId &&
}
130 | >
131 | )}
132 |
133 |
134 | );
135 | };
136 |
137 | export default App;
138 |
--------------------------------------------------------------------------------
/frontend/src/Components/Endpoint/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .endpointContainer {
4 | display: grid;
5 | grid-template-columns: 15% 57% 28%;
6 | width: 100%;
7 | border-top: 1px solid $black200;
8 | }
9 |
10 | .post {
11 | margin: 2 * $unit 0px 2 * $unit 3 * $unit;
12 | font-size: 1.4rem;
13 | }
14 |
15 | .endpointContents {
16 | margin: 3 * $unit 4 * $unit;
17 | }
18 |
19 | .endpointHeader {
20 | margin-bottom: $unit;
21 | }
22 |
23 | .endpointName {
24 | font-weight: bold;
25 | font-size: 2 * $unit;
26 | padding-right: $unit;
27 | }
28 |
29 | .schema {
30 | font-size: 2 * $unit;
31 | font-family: $font-stack-monospace;
32 | letter-spacing: -0.24px;
33 | }
34 |
35 | .endpointDescription {
36 | font-size: 1.4rem;
37 | line-height: 2 * $unit;
38 | }
39 |
40 | .buttonsContainer {
41 | display: flex;
42 | flex-direction: column;
43 | }
44 |
45 | .sendRequest {
46 | margin: 2 * $unit 2 * $unit $unit 3 * $unit;
47 | width: 70%;
48 | }
49 |
50 | .pdf {
51 | margin: 0 2 * $unit 0 3 * $unit;
52 | width: 70%;
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/Components/Endpoint/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Button from "plaid-threads/Button";
3 | import Note from "plaid-threads/Note";
4 |
5 | import Table from "../Table";
6 | import Error from "../Error";
7 | import { DataItem, Categories, ErrorDataItem, Data } from "../../dataUtilities";
8 |
9 | import styles from "./index.module.scss";
10 |
11 | interface Props {
12 | endpoint: string;
13 | name?: string;
14 | categories: Array;
15 | schema: string;
16 | description: string;
17 | transformData: (arg: any) => Array;
18 | }
19 |
20 | const Endpoint = (props: Props) => {
21 | const [showTable, setShowTable] = useState(false);
22 | const [transformedData, setTransformedData] = useState([]);
23 | const [pdf, setPdf] = useState(null);
24 | const [error, setError] = useState(null);
25 | const [isLoading, setIsLoading] = useState(false);
26 |
27 | const getData = async () => {
28 | setIsLoading(true);
29 | const response = await fetch(`/api/${props.endpoint}`, { method: "GET" });
30 | const data = await response.json();
31 | if (data.error != null) {
32 | setError(data.error);
33 | setIsLoading(false);
34 | return;
35 | }
36 | setTransformedData(props.transformData(data)); // transform data into proper format for each individual product
37 | if (data.pdf != null) {
38 | setPdf(data.pdf);
39 | }
40 | setShowTable(true);
41 | setIsLoading(false);
42 | };
43 |
44 | const getPdfName = () => {
45 | switch(props.name) {
46 | case 'Assets':
47 | return "Asset Report.pdf";
48 | case "CRA Base Report":
49 | return "Plaid Check Report.pdf";
50 | case "CRA Income Insights":
51 | return "Plaid Check Report with Insights.pdf";
52 | default:
53 | return "Statement.pdf";
54 | }
55 | };
56 |
57 | return (
58 | <>
59 |
60 |
61 | POST
62 |
63 |
64 |
65 | {props.name != null && (
66 | {props.name}
67 | )}
68 | {props.schema}
69 |
70 |
{props.description}
71 |
72 |
73 |
81 | {isLoading ? "Loading..." : `Send request`}
82 |
83 | {pdf != null && (
84 |
92 | Download PDF
93 |
94 | )}
95 |
96 |
97 | {showTable && (
98 |
103 | )}
104 | {error != null && }
105 | >
106 | );
107 | };
108 |
109 | Endpoint.displayName = "Endpoint";
110 |
111 | export default Endpoint;
112 |
--------------------------------------------------------------------------------
/frontend/src/Components/Error/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .errorTop {
4 | width: 90%;
5 | height: 1px;
6 | border-top: 1px solid $black200;
7 | }
8 |
9 | .errorContainer {
10 | display: grid;
11 | grid-template-columns: 15% 57% 28%;
12 | width: 100%;
13 | margin: 0;
14 | font-size: 1.4rem;
15 | }
16 |
17 | .code {
18 | margin: 2 * $unit 0px 2 * $unit 3 * $unit;
19 | font: $font-stack-monospace;
20 | font-size: 1.4rem;
21 | }
22 |
23 | .errorContents {
24 | margin: 3 * $unit 4 * $unit;
25 |
26 | }
27 |
28 | .errorItem {
29 | display: grid;
30 | grid-template-columns: 2fr 5fr;
31 | margin-bottom: $unit;
32 | }
33 |
34 | .errorTitle {
35 | font-weight: bold;
36 | line-height: normal;
37 | }
38 |
39 | .errorData {
40 | line-height: normal;
41 | font-family: $font-stack-monospace;
42 | letter-spacing: 0.25px;
43 | }
44 |
45 | .errorCode {
46 | display: inline-block;
47 | position: relative;
48 | }
49 |
50 | .pinkBox {
51 | position: absolute;
52 | top: 50%;
53 | height: 6px;
54 | width: 100%;
55 | background-color: $red200;
56 | z-index: -1;
57 | }
58 |
59 | .errorMessage {
60 | line-height: normal;
61 | }
62 |
63 | .learnMore {
64 | margin: 2 * $unit;
65 | margin-left: 3 * $unit;
66 | width: 70%;
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/Components/Error/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import Button from "plaid-threads/Button";
3 | import Note from "plaid-threads/Note";
4 |
5 | import { ErrorDataItem } from "../../dataUtilities";
6 |
7 | import styles from "./index.module.scss";
8 |
9 | interface Props {
10 | error: ErrorDataItem;
11 | }
12 |
13 | const errorPaths: { [key: string]: string } = {
14 | ITEM_ERROR: "item",
15 | INSTITUTION_ERROR: "institution",
16 | API_ERROR: "api",
17 | ASSET_REPORT_ERROR: "assets",
18 | BANK_TRANSFER_ERROR: "bank-transfers",
19 | INVALID_INPUT: "invalid-input",
20 | INVALID_REQUEST: "invalid-request",
21 | INVALID_RESULT: "invalid-result",
22 | OAUTH_ERROR: "oauth",
23 | PAYMENT_ERROR: "payment",
24 | RATE_LIMIT_EXCEEDED: "rate-limit-exceeded",
25 | RECAPTCHA_ERROR: "recaptcha",
26 | SANDBOX_ERROR: "sandbox",
27 | };
28 |
29 | const Error = (props: Props) => {
30 | const [path, setPath] = useState("");
31 |
32 | useEffect(() => {
33 | const errorType = props.error.error_type!;
34 | const errorPath = errorPaths[errorType];
35 |
36 | setPath(
37 | `https://plaid.com/docs/errors/${errorPath}/#${props.error.error_code?.toLowerCase()}`
38 | );
39 | }, [props.error]);
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 | {props.error.status_code ? props.error.status_code : "error"}
47 |
48 |
49 |
50 |
Error code:
51 |
52 |
53 | {props.error.error_code}
54 |
55 |
56 |
57 |
58 |
59 | Type:
60 | {props.error.error_type}
61 |
62 |
63 | Message:
64 |
65 | {props.error.display_message == null
66 | ? props.error.error_message
67 | : props.error.display_message}
68 |
69 |
70 |
71 |
78 | Learn more
79 |
80 |
81 | >
82 | );
83 | };
84 |
85 | Error.displayName = "Error";
86 |
87 | export default Error;
88 |
--------------------------------------------------------------------------------
/frontend/src/Components/Headers/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .grid {
4 | width: 100%;
5 | display: flex;
6 | flex-direction: column;
7 | margin: auto;
8 | margin-bottom: 2 * $unit;
9 | }
10 |
11 | .title {
12 | margin-top: 9 * $unit;
13 | margin-bottom: 0;
14 | font-weight: 800;
15 | height: 6 * $unit;
16 | }
17 |
18 | .subtitle {
19 | margin-top: 0;
20 | margin-bottom: 3 * $unit;
21 | }
22 |
23 | .introPar {
24 | width: 100%;
25 | font-size: 2 * $unit;
26 | margin: 2 * $unit 0;
27 | }
28 |
29 | .linkButton {
30 | margin-top: 3 * $unit;
31 | }
32 |
33 | .itemAccessContainer {
34 | display: flex;
35 | flex-direction: column;
36 | height: auto;
37 | width: 100%;
38 | box-shadow: $shadow-small;
39 | margin-top: 3 * $unit;
40 | border-radius: 2px;
41 | }
42 |
43 | .itemAccessRow {
44 | display: flex;
45 | border: 1px solid $black200;
46 | height: 50%;
47 | margin: 0;
48 |
49 | &:last-child {
50 | border-top: 0px;
51 | }
52 | }
53 |
54 | .idName {
55 | padding: 2 * $unit 3 * $unit 2 * $unit 5 * $unit;
56 | flex: 1;
57 | font-weight: bold;
58 | font-family: $font-stack-monospace;
59 | color: $black1000;
60 | }
61 |
62 | .tokenText {
63 | padding: 2 * $unit 3 * $unit 2 * $unit 0;
64 | flex: 5;
65 | font-family: $font-stack-monospace;
66 | }
67 |
68 | .requests {
69 | margin-top: 7 * $unit;
70 | font-size: 2 * $unit;
71 | }
72 |
--------------------------------------------------------------------------------
/frontend/src/Components/Headers/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import Callout from "plaid-threads/Callout";
3 | import Button from "plaid-threads/Button";
4 | import InlineLink from "plaid-threads/InlineLink";
5 |
6 | import Link from "../Link";
7 | import Context from "../../Context";
8 |
9 | import styles from "./index.module.scss";
10 |
11 | const Header = () => {
12 | const {
13 | itemId,
14 | accessToken,
15 | userToken,
16 | linkToken,
17 | linkSuccess,
18 | isItemAccess,
19 | backend,
20 | linkTokenError,
21 | isPaymentInitiation,
22 | } = useContext(Context);
23 |
24 | return (
25 |
26 |
Plaid Quickstart
27 |
28 | {!linkSuccess ? (
29 | <>
30 |
31 | A sample end-to-end integration with Plaid
32 |
33 |
34 | The Plaid flow begins when your user wants to connect their bank
35 | account to your app. Simulate this by clicking the button below to
36 | launch Link - the client-side component that your users will
37 | interact with in order to link their accounts to Plaid and allow you
38 | to access their accounts via the Plaid API.
39 |
40 | {/* message if backend is not running and there is no link token */}
41 | {!backend ? (
42 |
43 | Unable to fetch link_token: please make sure your backend server
44 | is running and that your .env file has been configured with your
45 | PLAID_CLIENT_ID
and PLAID_SECRET
.
46 |
47 | ) : /* message if backend is running and there is no link token */
48 | linkToken == null && backend ? (
49 |
50 |
51 | Unable to fetch link_token: please make sure your backend server
52 | is running and that your .env file has been configured
53 | correctly.
54 |
55 |
56 | If you are on a Windows machine, please ensure that you have
57 | cloned the repo with{" "}
58 |
62 | symlinks turned on.
63 | {" "}
64 | You can also try checking your{" "}
65 |
69 | activity log
70 | {" "}
71 | on your Plaid dashboard.
72 |
73 |
74 | Error Code: {linkTokenError.error_code}
75 |
76 |
77 | Error Type: {linkTokenError.error_type}
{" "}
78 |
79 | Error Message: {linkTokenError.error_message}
80 |
81 | ) : linkToken === "" ? (
82 |
83 |
84 | Loading...
85 |
86 |
87 | ) : (
88 |
89 |
90 |
91 | )}
92 | >
93 | ) : (
94 | <>
95 | {isPaymentInitiation ? (
96 | <>
97 |
98 | Congrats! Your payment is now confirmed.
99 |
100 |
101 | You can see information of all your payments in the{" "}
102 |
106 | Payments Dashboard
107 |
108 | .
109 |
110 |
111 |
112 | Now that the 'payment_id' stored in your server, you can use it
113 | to access the payment information:
114 |
115 | >
116 | ) : (
117 | /* If not using the payment_initiation product, show the item_id and access_token information */ <>
118 | {isItemAccess ? (
119 |
120 | Congrats! By linking an account, you have created an{" "}
121 |
125 | Item
126 |
127 | .
128 |
129 | ) : userToken ? (
130 |
131 | Congrats! You have successfully linked data to a User.
132 |
133 | ) : (
134 |
135 |
136 | Unable to create an item. Please check your backend server
137 |
138 |
139 | )}
140 |
141 | {itemId && (
142 |
143 | item_id
144 | {itemId}
145 |
146 | )}
147 |
148 | {accessToken && (
149 |
150 | access_token
151 | {accessToken}
152 |
153 | )}
154 |
155 | {userToken && (
156 |
157 | user_token
158 | {userToken}
159 |
160 | )}
161 |
162 | {(isItemAccess || userToken) && (
163 |
164 | Now that you have {accessToken && "an access_token"}
165 | {accessToken && userToken && " and "}
166 | {userToken && "a user_token"}, you can make all of the
167 | following requests:
168 |
169 | )}
170 | >
171 | )}
172 | >
173 | )}
174 |
175 | );
176 | };
177 |
178 | Header.displayName = "Header";
179 |
180 | export default Header;
181 |
--------------------------------------------------------------------------------
/frontend/src/Components/Link/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from "react";
2 | import { usePlaidLink } from "react-plaid-link";
3 | import Button from "plaid-threads/Button";
4 |
5 | import Context from "../../Context";
6 |
7 | const Link = () => {
8 | const { linkToken, isPaymentInitiation, isCraProductsExclusively, dispatch } =
9 | useContext(Context);
10 |
11 | const onSuccess = React.useCallback(
12 | (public_token: string) => {
13 | // If the access_token is needed, send public_token to server
14 | const exchangePublicTokenForAccessToken = async () => {
15 | const response = await fetch("/api/set_access_token", {
16 | method: "POST",
17 | headers: {
18 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
19 | },
20 | body: `public_token=${public_token}`,
21 | });
22 | if (!response.ok) {
23 | dispatch({
24 | type: "SET_STATE",
25 | state: {
26 | itemId: `no item_id retrieved`,
27 | accessToken: `no access_token retrieved`,
28 | isItemAccess: false,
29 | },
30 | });
31 | return;
32 | }
33 | const data = await response.json();
34 | dispatch({
35 | type: "SET_STATE",
36 | state: {
37 | itemId: data.item_id,
38 | accessToken: data.access_token,
39 | isItemAccess: true,
40 | },
41 | });
42 | };
43 |
44 | // 'payment_initiation' products do not require the public_token to be exchanged for an access_token.
45 | if (isPaymentInitiation) {
46 | dispatch({ type: "SET_STATE", state: { isItemAccess: false } });
47 | } else if (isCraProductsExclusively) {
48 | // When only CRA products are enabled, only user_token is needed. access_token/public_token exchange is not needed.
49 | dispatch({ type: "SET_STATE", state: { isItemAccess: false } });
50 | } else {
51 | exchangePublicTokenForAccessToken();
52 | }
53 |
54 | dispatch({ type: "SET_STATE", state: { linkSuccess: true } });
55 | window.history.pushState("", "", "/");
56 | },
57 | [dispatch, isPaymentInitiation, isCraProductsExclusively]
58 | );
59 |
60 | let isOauth = false;
61 | const config: Parameters[0] = {
62 | token: linkToken!,
63 | onSuccess,
64 | };
65 |
66 | if (window.location.href.includes("?oauth_state_id=")) {
67 | // TODO: figure out how to delete this ts-ignore
68 | // @ts-ignore
69 | config.receivedRedirectUri = window.location.href;
70 | isOauth = true;
71 | }
72 |
73 | const { open, ready } = usePlaidLink(config);
74 |
75 | useEffect(() => {
76 | if (isOauth && ready) {
77 | open();
78 | }
79 | }, [ready, open, isOauth]);
80 |
81 | return (
82 | open()} disabled={!ready}>
83 | Launch Link
84 |
85 | );
86 | };
87 |
88 | Link.displayName = "Link";
89 |
90 | export default Link;
91 |
--------------------------------------------------------------------------------
/frontend/src/Components/ProductTypes/Items.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Endpoint from "../Endpoint";
4 | import ProductTypesContainer from "./ProductTypesContainer";
5 | import {
6 | transformItemData,
7 | transformAccountsData,
8 | itemCategories,
9 | accountsCategories,
10 | } from "../../dataUtilities";
11 |
12 | const Items = () => (
13 | <>
14 |
15 |
24 |
31 |
32 | >
33 | );
34 |
35 | Items.displayName = "Items";
36 |
37 | export default Items;
38 |
--------------------------------------------------------------------------------
/frontend/src/Components/ProductTypes/ProductTypesContainer/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .container {
4 | width: 100%;
5 | border: 1px solid $black200;
6 | border-radius: 2px;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | box-shadow: $shadow-small;
11 | margin: 0 0 3 * $unit 0;
12 | }
13 |
14 | .header {
15 | width: 100%;
16 | border-bottom: 0px;
17 | height: 13 * $unit;
18 | font-weight: 800;
19 | padding-top: 5 * $unit;
20 | padding-left: 5 * $unit;
21 | margin-top: 0;
22 | margin-bottom: 0;
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/Components/ProductTypes/ProductTypesContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import styles from "./index.module.scss";
4 |
5 | interface Props {
6 | children?: React.ReactNode | Array;
7 | productType: string;
8 | }
9 |
10 | const TypeContainer: React.FC = (props) => (
11 |
12 |
{props.productType}
13 | {props.children}
14 |
15 | );
16 |
17 | TypeContainer.displayName = "TypeContainer";
18 |
19 | export default TypeContainer;
20 |
--------------------------------------------------------------------------------
/frontend/src/Components/ProductTypes/Products.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import Endpoint from "../Endpoint";
4 | import Context from "../../Context";
5 | import ProductTypesContainer from "./ProductTypesContainer";
6 | import {
7 | transactionsCategories,
8 | authCategories,
9 | identityCategories,
10 | balanceCategories,
11 | investmentsCategories,
12 | investmentsTransactionsCategories,
13 | liabilitiesCategories,
14 | paymentCategories,
15 | assetsCategories,
16 | incomePaystubsCategories,
17 | transferCategories,
18 | transferAuthorizationCategories,
19 | signalCategories,
20 | statementsCategories,
21 | transformAuthData,
22 | transformTransactionsData,
23 | transformBalanceData,
24 | transformInvestmentsData,
25 | transformInvestmentTransactionsData,
26 | transformLiabilitiesData,
27 | transformIdentityData,
28 | transformPaymentData,
29 | transformAssetsData,
30 | transformTransferData,
31 | transformTransferAuthorizationData,
32 | transformIncomePaystubsData,
33 | transformSignalData,
34 | transformStatementsData,
35 | transformBaseReportGetData,
36 | transformIncomeInsightsData,
37 | checkReportBaseReportCategories,
38 | checkReportInsightsCategories,
39 | transformPartnerInsightsData,
40 | checkReportPartnerInsightsCategories
41 | } from "../../dataUtilities";
42 |
43 | const Products = () => {
44 | const { products, isCraProductsExclusively } = useContext(Context);
45 | return (
46 |
47 | {products.includes("payment_initiation") && (
48 |
56 | )}
57 | {products.includes("auth") && (
58 |
66 | )}
67 | {products.includes("transactions") && (
68 |
76 | )}
77 | {products.includes("identity") && (
78 |
87 | )}
88 | {products.includes("assets") && (
89 |
97 | )}
98 | {!products.includes("payment_initiation") && !isCraProductsExclusively && (
99 |
108 | )}
109 | {products.includes("investments") && (
110 | <>
111 |
121 |
130 |
138 | >
139 | )}
140 | {products.includes("transfer") && (
141 | <>
142 |
150 |
158 | >
159 | )}
160 | {products.includes("signal") && (
161 | <>
162 |
170 | >
171 | )}
172 | {products.includes("statements") && (
173 | <>
174 |
182 | >
183 | )}
184 |
185 | {products.includes("income_verification") && (
186 |
194 | )}
195 |
196 | {(products.includes("cra_base_report") || products.includes("cra_income_insights")) && (
197 |
205 | )}
206 |
207 | {(products.includes("cra_base_report") || products.includes("cra_income_insights")) && (
208 |
216 | )}
217 |
218 | {products.includes("cra_partner_insights") && (
219 |
227 | )}
228 |
229 | );
230 | };
231 |
232 | Products.displayName = "Products";
233 |
234 | export default Products;
235 |
--------------------------------------------------------------------------------
/frontend/src/Components/Table/Identity.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .identityTable {
4 | width: 90%;
5 | margin-bottom: 3 * $unit;
6 | font-size: 1.4rem;
7 | line-height: normal;
8 | }
9 |
10 | .identityHeadersRow {
11 | display: grid;
12 | grid-template-columns: 25% 25% 25% 25%;
13 | grid-gap: $unit;
14 | border-top: 1px solid $black200;
15 | border-bottom: 1px solid $black1000;
16 | font-weight: bold;
17 | }
18 |
19 | .identityHeader {
20 | line-height: 2 * $unit;
21 | position: relative;
22 | bottom: 0;
23 | padding: $unit 0 0.4rem 0;
24 | }
25 |
26 | .identityDataBody {
27 | margin-top: 2 * $unit;
28 | }
29 |
30 | .identityDataRow {
31 | display: grid;
32 | grid-template-columns: 25% 25% 25% 25%;
33 | word-wrap: break-word;
34 | grid-gap: $unit;
35 | margin-bottom: 5px;
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/Components/Table/Identity.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { DataItem, Categories } from "../../dataUtilities";
4 |
5 | import styles from "./Identity.module.scss";
6 |
7 | interface Props {
8 | data: Array;
9 | categories: Array;
10 | }
11 |
12 | const Identity = (props: Props) => {
13 | const identityHeaders = props.categories.map((category, index) => (
14 |
15 | {category.title}
16 |
17 | ));
18 |
19 | const identityRows = props.data.map((item: DataItem | any, index) => (
20 |
21 | {props.categories.map((category: Categories, index) => (
22 |
23 | {item[category.field]}
24 |
25 | ))}
26 |
27 | ));
28 |
29 | return (
30 |
31 |
{identityHeaders}
32 |
{identityRows}
33 |
34 | );
35 | };
36 |
37 | Identity.displayName = "Identity";
38 |
39 | export default Identity;
40 |
--------------------------------------------------------------------------------
/frontend/src/Components/Table/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "~plaid-threads/scss/variables";
2 |
3 | .dataTable {
4 | width: 90%;
5 | margin-bottom: 3 * $unit;
6 | border-spacing: 0px;
7 | font-size: 1.4rem;
8 | }
9 |
10 | .headerRow {
11 | text-align: left;
12 | height: 4 * $unit;
13 | }
14 |
15 | .headerField {
16 | border-top: 1px solid $black200;
17 | border-bottom: 1px solid $black1000;
18 | padding-top: 1.2rem;
19 | padding-bottom: $unit;
20 | padding-left: $unit;
21 | }
22 |
23 | .dataRows {
24 | height: 6 * $unit;
25 | &:first-child {
26 | td {
27 | border-top: 0px;
28 | }
29 | }
30 | &:last-child {
31 | margin-bottom: 2 * $unit;
32 | }
33 | }
34 |
35 | .dataField {
36 | text-align: left;
37 | overflow-wrap: break-word;
38 | border-top: 1px solid $black200;
39 | max-width: 25 * $unit;
40 | vertical-align: top;
41 | padding-top: 2 * $unit;
42 | padding-left: $unit;
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/Components/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { DataItem, Categories } from "../../dataUtilities";
4 | import Identity from "./Identity";
5 |
6 | import styles from "./index.module.scss";
7 |
8 | interface Props {
9 | data: Array;
10 | categories: Array;
11 | isIdentity: boolean;
12 | }
13 |
14 | const Table = (props: Props) => {
15 | const maxRows = 15;
16 | // regular table
17 | const headers = props.categories.map((category, index) => (
18 |
19 | {category.title}
20 |
21 | ));
22 |
23 | const rows = props.data
24 | .map((item: DataItem | any, index) => (
25 |
26 | {props.categories.map((category: Categories, index) => (
27 |
28 | {item[category.field]}
29 |
30 | ))}
31 |
32 | ))
33 | .slice(0, maxRows);
34 |
35 | return props.isIdentity ? (
36 |
37 | ) : (
38 |
39 |
40 | {headers}
41 |
42 | {rows}
43 |
44 | );
45 | };
46 |
47 | Table.displayName = "Table";
48 |
49 | export default Table;
50 |
--------------------------------------------------------------------------------
/frontend/src/Context/index.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useReducer, Dispatch, ReactNode } from "react";
2 |
3 | interface QuickstartState {
4 | linkSuccess: boolean;
5 | isItemAccess: boolean;
6 | isPaymentInitiation: boolean;
7 | isUserTokenFlow: boolean;
8 | isCraProductsExclusively: boolean;
9 | linkToken: string | null;
10 | accessToken: string | null;
11 | userToken: string | null;
12 | itemId: string | null;
13 | isError: boolean;
14 | backend: boolean;
15 | products: string[];
16 | linkTokenError: {
17 | error_message: string;
18 | error_code: string;
19 | error_type: string;
20 | };
21 | }
22 |
23 | const initialState: QuickstartState = {
24 | linkSuccess: false,
25 | isItemAccess: true,
26 | isPaymentInitiation: false,
27 | isCraProductsExclusively: false,
28 | isUserTokenFlow: false,
29 | linkToken: "", // Don't set to null or error message will show up briefly when site loads
30 | userToken: null,
31 | accessToken: null,
32 | itemId: null,
33 | isError: false,
34 | backend: true,
35 | products: ["transactions"],
36 | linkTokenError: {
37 | error_type: "",
38 | error_code: "",
39 | error_message: "",
40 | },
41 | };
42 |
43 | type QuickstartAction = {
44 | type: "SET_STATE";
45 | state: Partial;
46 | };
47 |
48 | interface QuickstartContext extends QuickstartState {
49 | dispatch: Dispatch;
50 | }
51 |
52 | const Context = createContext(
53 | initialState as QuickstartContext
54 | );
55 |
56 | const { Provider } = Context;
57 | export const QuickstartProvider: React.FC<{ children: ReactNode }> = (
58 | props
59 | ) => {
60 | const reducer = (
61 | state: QuickstartState,
62 | action: QuickstartAction
63 | ): QuickstartState => {
64 | switch (action.type) {
65 | case "SET_STATE":
66 | return { ...state, ...action.state };
67 | default:
68 | return { ...state };
69 | }
70 | };
71 | const [state, dispatch] = useReducer(reducer, initialState);
72 | return {props.children} ;
73 | };
74 |
75 | export default Context;
76 |
--------------------------------------------------------------------------------
/frontend/src/dataUtilities.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AccountsGetResponse,
3 | AssetReport,
4 | AuthGetResponse,
5 | CraCheckReportBaseReportGetResponse,
6 | CraCheckReportIncomeInsightsGetResponse,
7 | CraCheckReportPartnerInsightsGetResponse,
8 | IdentityGetResponse,
9 | IncomeVerificationPaystubsGetResponse,
10 | InstitutionsGetByIdResponse,
11 | InvestmentsHoldingsGetResponse,
12 | InvestmentsTransactionsGetResponse,
13 | ItemGetResponse,
14 | LiabilitiesGetResponse,
15 | PaymentInitiationPaymentGetResponse,
16 | Paystub,
17 | SignalEvaluateResponse,
18 | StatementsListResponse,
19 | Transaction,
20 | TransferAuthorizationCreateResponse,
21 | TransferCreateResponse,
22 | } from "plaid/dist/api";
23 |
24 | const formatCurrency = (
25 | number: number | null | undefined,
26 | code: string | null | undefined
27 | ) => {
28 | if (number != null && number !== undefined) {
29 | return ` ${parseFloat(number.toFixed(2)).toLocaleString("en")} ${code}`;
30 | }
31 | return "no data";
32 | };
33 |
34 | export interface Categories {
35 | title: string;
36 | field: string;
37 | }
38 |
39 | //interfaces for categories in each individual product
40 | interface AuthDataItem {
41 | routing: string;
42 | account: string;
43 | balance: string;
44 | name: string;
45 | }
46 | interface TransactionsDataItem {
47 | amount: string;
48 | date: string;
49 | name: string;
50 | }
51 |
52 | interface IdentityDataItem {
53 | addresses: string;
54 | phoneNumbers: string;
55 | emails: string;
56 | names: string;
57 | }
58 |
59 | interface BalanceDataItem {
60 | balance: string;
61 | subtype: string | null;
62 | mask: string;
63 | name: string;
64 | }
65 |
66 | interface InvestmentsDataItem {
67 | mask: string;
68 | quantity: string;
69 | price: string;
70 | value: string;
71 | name: string;
72 | }
73 |
74 | interface InvestmentsTransactionItem {
75 | amount: number;
76 | date: string;
77 | name: string;
78 | }
79 |
80 | interface LiabilitiessDataItem {
81 | amount: string;
82 | date: string;
83 | name: string;
84 | type: string;
85 | }
86 |
87 | interface PaymentDataItem {
88 | paymentId: string;
89 | amount: string;
90 | status: string;
91 | statusUpdate: string;
92 | recipientId: string;
93 | }
94 | interface ItemDataItem {
95 | billed: string;
96 | available: string;
97 | name: string;
98 | }
99 |
100 | interface AssetsDataItem {
101 | account: string;
102 | balance: string;
103 | transactions: number;
104 | daysAvailable: number;
105 | }
106 |
107 | interface TransferDataItem {
108 | transferId: string;
109 | amount: string;
110 | type: string;
111 | achClass: string | null;
112 | network: string;
113 | }
114 |
115 | interface TransferAuthorizationDataItem {
116 | authorizationId: string;
117 | authorizationDecision: string;
118 | decisionRationaleCode: string | null;
119 | decisionRationaleDescription: string | null;
120 | }
121 |
122 | interface StatementsDataItem {
123 | account: string | null;
124 | date: string | null;
125 | }
126 |
127 | interface SignalDataItem {
128 | customerInitiatedReturnRiskScore: number | undefined | null;
129 | customerInitiatedReturnRiskTier: number | undefined | null;
130 | bankInitiatedReturnRiskScore: number | undefined | null;
131 | bankInitiatedReturnRiskTier: number | undefined | null;
132 | daysSinceFirstPlaidConnection: number | undefined | null;
133 | }
134 |
135 | interface IncomePaystubsDataItem {
136 | description: string;
137 | currentAmount: number | null;
138 | currency: number | null;
139 | }
140 |
141 | interface CreditReportGetItem {
142 | institution: string;
143 | accountName: string;
144 | averageDaysBetweenTransactions: string | null;
145 | averageInflowAmount: string | null;
146 | averageOutflowAmount: string | null;
147 | averageBalance: string | null;
148 | balance: string | null;
149 | }
150 |
151 | interface CreditInsightsGetItem {
152 | incomeSourcesCount: number | null;
153 | historicalAnnualIncome: string | null;
154 | forecastedAnnualIncome: string | null;
155 | }
156 |
157 | interface CreditPartnerInsightsGetItem {
158 | firstDetectScore: number | null;
159 | cashScore: number | null;
160 | }
161 |
162 | export interface ErrorDataItem {
163 | error_type: string;
164 | error_code: string;
165 | error_message: string;
166 | display_message: string | null;
167 | status_code: number | null;
168 | }
169 |
170 | //all possible product data interfaces
171 | export type DataItem =
172 | | AuthDataItem
173 | | TransactionsDataItem
174 | | IdentityDataItem
175 | | BalanceDataItem
176 | | InvestmentsDataItem
177 | | InvestmentsTransactionItem
178 | | LiabilitiessDataItem
179 | | ItemDataItem
180 | | PaymentDataItem
181 | | AssetsDataItem
182 | | TransferDataItem
183 | | TransferAuthorizationDataItem
184 | | IncomePaystubsDataItem
185 | | SignalDataItem
186 | | StatementsDataItem
187 | | CreditReportGetItem
188 | | CreditInsightsGetItem
189 | | CreditPartnerInsightsGetItem;
190 |
191 | export type Data = Array;
192 |
193 | export const authCategories: Array = [
194 | {
195 | title: "Name",
196 | field: "name",
197 | },
198 | {
199 | title: "Balance",
200 | field: "balance",
201 | },
202 | {
203 | title: "Account #",
204 | field: "account",
205 | },
206 | {
207 | title: "Routing #",
208 | field: "routing",
209 | },
210 | ];
211 |
212 | export const transactionsCategories: Array = [
213 | {
214 | title: "Name",
215 | field: "name",
216 | },
217 | {
218 | title: "Amount",
219 | field: "amount",
220 | },
221 | {
222 | title: "Date",
223 | field: "date",
224 | },
225 | ];
226 |
227 | export const identityCategories: Array = [
228 | {
229 | title: "Names",
230 | field: "names",
231 | },
232 | {
233 | title: "Emails",
234 | field: "emails",
235 | },
236 | {
237 | title: "Phone numbers",
238 | field: "phoneNumbers",
239 | },
240 | {
241 | title: "Addresses",
242 | field: "addresses",
243 | },
244 | ];
245 |
246 | export const balanceCategories: Array = [
247 | {
248 | title: "Name",
249 | field: "name",
250 | },
251 | {
252 | title: "Balance",
253 | field: "balance",
254 | },
255 | {
256 | title: "Subtype",
257 | field: "subtype",
258 | },
259 | {
260 | title: "Mask",
261 | field: "mask",
262 | },
263 | ];
264 |
265 | export const investmentsCategories: Array = [
266 | {
267 | title: "Account Mask",
268 | field: "mask",
269 | },
270 | {
271 | title: "Name",
272 | field: "name",
273 | },
274 | {
275 | title: "Quantity",
276 | field: "quantity",
277 | },
278 | {
279 | title: "Close Price",
280 | field: "price",
281 | },
282 | {
283 | title: "Value",
284 | field: "value",
285 | },
286 | ];
287 |
288 | export const investmentsTransactionsCategories: Array = [
289 | {
290 | title: "Name",
291 | field: "name",
292 | },
293 | {
294 | title: "Amount",
295 | field: "amount",
296 | },
297 | {
298 | title: "Date",
299 | field: "date",
300 | },
301 | ];
302 |
303 | export const liabilitiesCategories: Array = [
304 | {
305 | title: "Name",
306 | field: "name",
307 | },
308 | {
309 | title: "Type",
310 | field: "type",
311 | },
312 | {
313 | title: "Last Payment Date",
314 | field: "date",
315 | },
316 | {
317 | title: "Last Payment Amount",
318 | field: "amount",
319 | },
320 | ];
321 |
322 | export const itemCategories: Array = [
323 | {
324 | title: "Institution Name",
325 | field: "name",
326 | },
327 | {
328 | title: "Billed Products",
329 | field: "billed",
330 | },
331 | {
332 | title: "Available Products",
333 | field: "available",
334 | },
335 | ];
336 |
337 | export const accountsCategories: Array = [
338 | {
339 | title: "Name",
340 | field: "name",
341 | },
342 | {
343 | title: "Balance",
344 | field: "balance",
345 | },
346 | {
347 | title: "Subtype",
348 | field: "subtype",
349 | },
350 | {
351 | title: "Mask",
352 | field: "mask",
353 | },
354 | ];
355 |
356 | export const paymentCategories: Array = [
357 | {
358 | title: "Payment ID",
359 | field: "paymentId",
360 | },
361 | {
362 | title: "Amount",
363 | field: "amount",
364 | },
365 | {
366 | title: "Status",
367 | field: "status",
368 | },
369 | {
370 | title: "Status Update",
371 | field: "statusUpdate",
372 | },
373 | {
374 | title: "Recipient ID",
375 | field: "recipientId",
376 | },
377 | ];
378 |
379 | export const assetsCategories: Array = [
380 | {
381 | title: "Account",
382 | field: "account",
383 | },
384 | {
385 | title: "Transactions",
386 | field: "transactions",
387 | },
388 | {
389 | title: "Balance",
390 | field: "balance",
391 | },
392 | {
393 | title: "Days Available",
394 | field: "daysAvailable",
395 | },
396 | ];
397 |
398 | export const transferCategories: Array = [
399 | {
400 | title: "Transfer ID",
401 | field: "transferId",
402 | },
403 | {
404 | title: "Amount",
405 | field: "amount",
406 | },
407 | {
408 | title: "Type",
409 | field: "type",
410 | },
411 | {
412 | title: "ACH Class",
413 | field: "achClass",
414 | },
415 | {
416 | title: "Network",
417 | field: "network",
418 | },
419 | {
420 | title: "Status",
421 | field: "status",
422 | },
423 | ];
424 |
425 | export const transferAuthorizationCategories: Array = [
426 | {
427 | title: "Authorization ID",
428 | field: "authorizationId",
429 | },
430 | {
431 | title: "Authorization Decision",
432 | field: "authorizationDecision",
433 | },
434 | {
435 | title: "Decision rationale code",
436 | field: "decisionRationaleCode",
437 | },
438 | {
439 | title: "Decision rationale description",
440 | field: "decisionRationaleDescription",
441 | },
442 | ];
443 |
444 | export const signalCategories: Array = [
445 | {
446 | title: "Customer-initiated return risk score",
447 | field: "customerInitiatedReturnRiskScore",
448 | },
449 |
450 | {
451 | title: "Customer-initiated return risk tier",
452 | field: "customerInitiatedReturnRiskTier",
453 | },
454 | {
455 | title: "Bank-initiated return risk score",
456 | field: "bankInitiatedReturnRiskScore",
457 | },
458 | {
459 | title: "Bank-initiated return risk tier",
460 | field: "bankInitiatedReturnRiskTier",
461 | },
462 | {
463 | title: "Sample core attribute: Days since first Plaid connection",
464 | field: "daysSinceFirstPlaidConnection",
465 | },
466 | ];
467 |
468 | export const statementsCategories: Array = [
469 | {
470 | title: "Account name",
471 | field: "account"
472 | },
473 | {
474 | title: "Statement Date",
475 | field: "date"
476 | }
477 | ];
478 |
479 | export const incomePaystubsCategories: Array = [
480 | {
481 | title: "Description",
482 | field: "description",
483 | },
484 | {
485 | title: "Current Amount",
486 | field: "currentAmount",
487 | },
488 | {
489 | title: "Currency",
490 | field: "currency",
491 | },
492 | ];
493 |
494 |
495 | export const checkReportBaseReportCategories: Array = [
496 | {
497 | title: "Account Name",
498 | field: "accountName"
499 | },
500 | {
501 | title: "Balance",
502 | field: "balance"
503 | },
504 | {
505 | title: "Avg. Balance",
506 | field: "averageBalance"
507 | },
508 | {
509 | title: "Avg. Inflow Amount",
510 | field: "averageInflowAmount"
511 | },
512 | {
513 | title: "Avg. Outflow Amount",
514 | field: "averageOutflowAmount"
515 | },
516 | {
517 | title: "Avg. Days Between Transactions",
518 | field: "averageDaysBetweenTransactions"
519 | }
520 | ];
521 |
522 | export const checkReportInsightsCategories: Array = [
523 | {
524 | title: "Income Sources",
525 | field: "incomeSourcesCount",
526 | },
527 | {
528 | title: "Historical Annual Income",
529 | field: "historicalAnnualIncome",
530 | },
531 | {
532 | title: "Forecasted Annual Income",
533 | field: "forecastedAnnualIncome",
534 | }
535 | ];
536 |
537 | export const checkReportPartnerInsightsCategories: Array = [
538 | {
539 | title: "CashScore®",
540 | field: "cashScore",
541 | },
542 | {
543 | title: "FirstDetect Score",
544 | field: "firstDetectScore",
545 | }
546 | ];
547 |
548 | export const transformAuthData = (data: AuthGetResponse) => {
549 | return data.numbers.ach!.map((achNumbers) => {
550 | const account = data.accounts!.filter((a) => {
551 | return a.account_id === achNumbers.account_id;
552 | })[0];
553 | const balance: number | null | undefined =
554 | account.balances.available || account.balances.current;
555 | const obj: DataItem = {
556 | name: account.name,
557 | balance: formatCurrency(balance, account.balances.iso_currency_code),
558 | account: achNumbers.account!,
559 | routing: achNumbers.routing!,
560 | };
561 | return obj;
562 | });
563 | };
564 |
565 | export const transformStatementsData = (data: {json: StatementsListResponse}) => {
566 | const account = data.json.accounts[0]!.account_name;
567 | const statements = data.json.accounts[0]!.statements;
568 | return statements!.map((s) => {
569 | const item: DataItem = {
570 | date: Intl.DateTimeFormat('en', { month: 'long', year:'numeric' }).format(new Date(s.year!, s.month!)),
571 | account: account,
572 | };
573 | return item;
574 | });
575 | };
576 |
577 | export const transformTransactionsData = (data: {
578 | latest_transactions: Transaction[];
579 | }): Array => {
580 | return data.latest_transactions!.map((t) => {
581 | const item: DataItem = {
582 | name: t.name!,
583 | amount: formatCurrency(t.amount!, t.iso_currency_code),
584 | date: t.date,
585 | };
586 | return item;
587 | });
588 | };
589 |
590 | interface IdentityData {
591 | identity: IdentityGetResponse["accounts"];
592 | }
593 |
594 | export const transformIdentityData = (data: IdentityData) => {
595 | const final: Array = [];
596 | const identityData = data.identity![0];
597 | identityData.owners.forEach((owner) => {
598 | const names = owner.names.map((name) => {
599 | return name;
600 | });
601 | const emails = owner.emails.map((email) => {
602 | return email.data;
603 | });
604 | const phones = owner.phone_numbers.map((phone) => {
605 | return phone.data;
606 | });
607 | const addresses = owner.addresses.map((address) => {
608 | return `${address.data.street} ${address.data.city}, ${address.data.region} ${address.data.postal_code}`;
609 | });
610 |
611 | const num = Math.max(
612 | emails.length,
613 | names.length,
614 | phones.length,
615 | addresses.length
616 | );
617 |
618 | for (let i = 0; i < num; i++) {
619 | const obj = {
620 | names: names[i] || "",
621 | emails: emails[i] || "",
622 | phoneNumbers: phones[i] || "",
623 | addresses: addresses[i] || "",
624 | };
625 | final.push(obj);
626 | }
627 | });
628 |
629 | return final;
630 | };
631 |
632 | export const transformBalanceData = (data: AccountsGetResponse) => {
633 | const balanceData = data.accounts;
634 | return balanceData.map((account) => {
635 | const balance: number | null | undefined =
636 | account.balances.available || account.balances.current;
637 | const obj: DataItem = {
638 | name: account.name,
639 | balance: formatCurrency(balance, account.balances.iso_currency_code),
640 | subtype: account.subtype,
641 | mask: account.mask!,
642 | };
643 | return obj;
644 | });
645 | };
646 |
647 | interface InvestmentData {
648 | error: null;
649 | holdings: InvestmentsHoldingsGetResponse;
650 | }
651 |
652 | export const transformInvestmentsData = (data: InvestmentData) => {
653 | const holdingsData = data.holdings.holdings!.sort(function (a, b) {
654 | if (a.account_id > b.account_id) return 1;
655 | return -1;
656 | });
657 | return holdingsData.map((holding) => {
658 | const account = data.holdings.accounts!.filter(
659 | (acc) => acc.account_id === holding.account_id
660 | )[0];
661 | const security = data.holdings.securities!.filter(
662 | (sec) => sec.security_id === holding.security_id
663 | )[0];
664 | const value = holding.quantity * security.close_price!;
665 |
666 | const obj: DataItem = {
667 | mask: account.mask!,
668 | name: security.name!,
669 | quantity: formatCurrency(holding.quantity, ""),
670 | price: formatCurrency(
671 | security.close_price!,
672 | account.balances.iso_currency_code
673 | ),
674 | value: formatCurrency(value, account.balances.iso_currency_code),
675 | };
676 | return obj;
677 | });
678 | };
679 |
680 | interface InvestmentsTransactionData {
681 | error: null;
682 | investments_transactions: InvestmentsTransactionsGetResponse;
683 | }
684 |
685 | export const transformInvestmentTransactionsData = (
686 | data: InvestmentsTransactionData
687 | ) => {
688 | const investmentTransactionsData =
689 | data.investments_transactions.investment_transactions!.sort(function (
690 | a,
691 | b
692 | ) {
693 | if (a.account_id > b.account_id) return 1;
694 | return -1;
695 | });
696 | return investmentTransactionsData.map((investmentTransaction) => {
697 | const security = data.investments_transactions.securities!.filter(
698 | (sec) => sec.security_id === investmentTransaction.security_id
699 | )[0];
700 |
701 | const obj: DataItem = {
702 | name: security.name!,
703 | amount: investmentTransaction.amount,
704 | date: investmentTransaction.date,
705 | };
706 | return obj;
707 | });
708 | };
709 |
710 | interface LiabilitiesDataResponse {
711 | error: null;
712 | liabilities: LiabilitiesGetResponse;
713 | }
714 |
715 | export const transformLiabilitiesData = (data: LiabilitiesDataResponse) => {
716 | const liabilitiesData = data.liabilities.liabilities;
717 | //console.log(liabilitiesData)
718 | //console.log("random")
719 | const credit = liabilitiesData.credit!.map((credit) => {
720 | const account = data.liabilities.accounts.filter(
721 | (acc) => acc.account_id === credit.account_id
722 | )[0];
723 | const obj: DataItem = {
724 | name: account.name,
725 | type: "credit card",
726 | date: credit.last_payment_date ?? "",
727 | amount: formatCurrency(
728 | credit.last_payment_amount,
729 | account.balances.iso_currency_code
730 | ),
731 | };
732 | return obj;
733 | });
734 |
735 | const mortgages = liabilitiesData.mortgage?.map((mortgage) => {
736 | const account = data.liabilities.accounts.filter(
737 | (acc) => acc.account_id === mortgage.account_id
738 | )[0];
739 | const obj: DataItem = {
740 | name: account.name,
741 | type: "mortgage",
742 | date: mortgage.last_payment_date!,
743 | amount: formatCurrency(
744 | mortgage.last_payment_amount!,
745 | account.balances.iso_currency_code
746 | ),
747 | };
748 | return obj;
749 | });
750 |
751 | const student = liabilitiesData.student?.map((student) => {
752 | const account = data.liabilities.accounts.filter(
753 | (acc) => acc.account_id === student.account_id
754 | )[0];
755 | const obj: DataItem = {
756 | name: account.name,
757 | type: "student loan",
758 | date: student.last_payment_date!,
759 | amount: formatCurrency(
760 | student.last_payment_amount!,
761 | account.balances.iso_currency_code
762 | ),
763 | };
764 | return obj;
765 | });
766 |
767 | return credit!.concat(mortgages!).concat(student!);
768 | };
769 |
770 | export const transformSignalData = (data: SignalEvaluateResponse) => {
771 | return [
772 | {
773 | customerInitiatedReturnRiskTier:
774 | data.scores.customer_initiated_return_risk!.risk_tier,
775 | customerInitiatedReturnRiskScore:
776 | data.scores.customer_initiated_return_risk!.score,
777 | bankInitiatedReturnRiskTier:
778 | data.scores.bank_initiated_return_risk!.risk_tier,
779 | bankInitiatedReturnRiskScore:
780 | data.scores.bank_initiated_return_risk!.score,
781 | daysSinceFirstPlaidConnection:
782 | data.core_attributes!.days_since_first_plaid_connection,
783 | },
784 | ];
785 | };
786 |
787 | export const transformTransferAuthorizationData = (
788 | data: TransferAuthorizationCreateResponse
789 | ): Array => {
790 | const transferAuthorizationData = data.authorization;
791 | return [
792 | {
793 | authorizationId: transferAuthorizationData.id,
794 | authorizationDecision: transferAuthorizationData.decision,
795 | decisionRationaleCode:
796 | transferAuthorizationData.decision_rationale != null
797 | ? transferAuthorizationData.decision_rationale.code
798 | : "null",
799 | decisionRationaleDescription:
800 | transferAuthorizationData.decision_rationale != null
801 | ? transferAuthorizationData.decision_rationale.description
802 | : "null",
803 | },
804 | ];
805 | };
806 |
807 | export const transformTransferData = (
808 | data: TransferCreateResponse
809 | ): Array => {
810 | const transferData = data.transfer;
811 | return [
812 | {
813 | transferId: transferData.id,
814 | amount: transferData.amount,
815 | type: transferData.type,
816 | achClass: transferData.ach_class || null,
817 | network: transferData.network,
818 | status: transferData.status,
819 | },
820 | ];
821 | };
822 |
823 | interface ItemData {
824 | item: ItemGetResponse["item"];
825 | institution: InstitutionsGetByIdResponse["institution"];
826 | }
827 |
828 | export const transformItemData = (data: ItemData): Array => {
829 | return [
830 | {
831 | name: data.institution.name,
832 | billed: data.item.billed_products.join(", "),
833 | available: data.item.available_products.join(", "),
834 | },
835 | ];
836 | };
837 |
838 | export const transformAccountsData = (data: AccountsGetResponse) => {
839 | const accountsData = data.accounts;
840 | return accountsData.map((account) => {
841 | const balance: number | null | undefined =
842 | account.balances.available || account.balances.current;
843 | const obj: DataItem = {
844 | name: account.name,
845 | balance: formatCurrency(balance, account.balances.iso_currency_code),
846 | subtype: account.subtype,
847 | mask: account.mask!,
848 | };
849 | return obj;
850 | });
851 | };
852 |
853 | interface PaymentData {
854 | payment: PaymentInitiationPaymentGetResponse;
855 | }
856 |
857 | export const transformPaymentData = (data: PaymentData) => {
858 | const statusUpdate =
859 | typeof data.payment.last_status_update === "string"
860 | ? data.payment.last_status_update.replace("T", " ").replace("Z", "")
861 | : new Date(data.payment.last_status_update * 1000) // Java data comes as timestamp
862 | .toISOString()
863 | .replace("T", " ")
864 | .replace("Z", "");
865 | return [
866 | {
867 | paymentId: data.payment.payment_id,
868 | amount: `${data.payment.amount.currency} ${data.payment.amount.value}`,
869 | status: data.payment.status,
870 | statusUpdate: statusUpdate,
871 | recipientId: data.payment.recipient_id,
872 | },
873 | ];
874 | };
875 |
876 | interface AssetResponseData {
877 | json: AssetReport;
878 | }
879 |
880 | export const transformAssetsData = (data: AssetResponseData) => {
881 | const assetItems = data.json.items;
882 | return assetItems.flatMap((item) => {
883 | return item.accounts.map((account) => {
884 | const balance: number | null | undefined =
885 | account.balances.available || account.balances.current;
886 | const obj: DataItem = {
887 | account: account.name,
888 | balance: formatCurrency(balance, account.balances.iso_currency_code),
889 | transactions: account.transactions!.length,
890 | daysAvailable: account.days_available!,
891 | };
892 | return obj;
893 | });
894 | });
895 | };
896 |
897 | interface IncomePaystub {
898 | paystubs: IncomeVerificationPaystubsGetResponse;
899 | }
900 |
901 | export const transformIncomePaystubsData = (data: IncomePaystub) => {
902 | const paystubsItemsArray: Array = data.paystubs.paystubs;
903 | var finalArray: Array = [];
904 | for (var i = 0; i < paystubsItemsArray.length; i++) {
905 | var ActualEarningVariable: any = paystubsItemsArray[i].earnings;
906 | for (var j = 0; j < ActualEarningVariable.breakdown.length; j++) {
907 | var payStubItem: IncomePaystubsDataItem = {
908 | description:
909 | paystubsItemsArray[i].employer.name +
910 | "_" +
911 | ActualEarningVariable.breakdown[j].description,
912 | currentAmount: ActualEarningVariable.breakdown[j].current_amount,
913 | currency: ActualEarningVariable.breakdown[j].iso_currency_code,
914 | };
915 | finalArray.push(payStubItem);
916 | }
917 | }
918 | return finalArray;
919 | };
920 |
921 | export const transformBaseReportGetData = (data: CraCheckReportBaseReportGetResponse) => {
922 | const report = data.report;
923 | return report.items.flatMap((item) =>
924 | item.accounts.map((account) => {
925 | const accountInsights = account.account_insights;
926 | const averageInflow = accountInsights?.average_inflow_amount?.pop()?.total_amount;
927 | const averageOutflow = accountInsights?.average_outflow_amount?.pop()?.total_amount;
928 | return {
929 | accountName: account.name,
930 | averageDaysBetweenTransactions: accountInsights?.average_days_between_transactions?.toFixed(2),
931 | averageInflowAmount: formatCurrency(averageInflow?.amount, averageInflow?.iso_currency_code),
932 | averageOutflowAmount: formatCurrency(averageOutflow?.amount, averageOutflow?.iso_currency_code),
933 | averageBalance: formatCurrency(account.balances.average_balance, account.balances.iso_currency_code),
934 | balance: formatCurrency(account.balances.available, account.balances.iso_currency_code)
935 | };
936 | })) as Array;
937 | };
938 |
939 |
940 | export const transformIncomeInsightsData = (data: CraCheckReportIncomeInsightsGetResponse) => {
941 | const report = data.report?.bank_income_summary
942 | const historicalIncome = report?.historical_annual_income?.pop()
943 | const forecastedIncome = report?.forecasted_annual_income?.pop()
944 | return [
945 | {
946 | incomeSourcesCount: report?.income_sources_count,
947 | historicalAnnualIncome: formatCurrency(historicalIncome?.amount, historicalIncome?.iso_currency_code),
948 | forecastedAnnualIncome: formatCurrency(forecastedIncome?.amount, forecastedIncome?.iso_currency_code)
949 | }
950 | ] as Array;
951 | };
952 |
953 |
954 | export const transformPartnerInsightsData = (data: CraCheckReportPartnerInsightsGetResponse) => {
955 | const report = data.report?.prism
956 | return [
957 | {
958 | cashScore: report?.cash_score?.score,
959 | firstDetectScore: report?.first_detect?.score,
960 | }
961 | ] as Array;
962 | };
963 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import { QuickstartProvider } from "./Context";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.getElementById("root")
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/frontend/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const { createProxyMiddleware } = require('http-proxy-middleware');
2 |
3 | module.exports = function(app) {
4 | app.use(
5 | '/api',
6 | createProxyMiddleware({
7 | target: process.env.REACT_APP_API_HOST || 'http://127.0.0.1:8000',
8 | changeOrigin: true,
9 | })
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src", "index.js"]
20 | }
21 |
--------------------------------------------------------------------------------
/go/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/go/.env.example:
--------------------------------------------------------------------------------
1 | ../.env.example
--------------------------------------------------------------------------------
/go/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22 AS build
2 |
3 | WORKDIR /opt/src
4 | COPY . .
5 | WORKDIR /opt/src/go
6 |
7 | RUN go get -d -v ./...
8 | RUN go build -o quickstart
9 |
10 | FROM gcr.io/distroless/base-debian12
11 |
12 | COPY --from=build /opt/src/go/quickstart /
13 | COPY .env /.env
14 |
15 | EXPOSE 8000
16 | ENTRYPOINT ["/quickstart"]
17 |
--------------------------------------------------------------------------------
/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/plaid/quickstart
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.10.0
7 | github.com/joho/godotenv v1.5.1
8 | github.com/plaid/plaid-go/v31 v31.0.0
9 | )
10 |
11 | require (
12 | github.com/bytedance/sonic v1.11.6 // indirect
13 | github.com/bytedance/sonic/loader v0.1.1 // indirect
14 | github.com/cloudwego/base64x v0.1.4 // indirect
15 | github.com/cloudwego/iasm v0.2.0 // indirect
16 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
17 | github.com/gin-contrib/sse v0.1.0 // indirect
18 | github.com/go-playground/locales v0.14.1 // indirect
19 | github.com/go-playground/universal-translator v0.18.1 // indirect
20 | github.com/go-playground/validator/v10 v10.20.0 // indirect
21 | github.com/goccy/go-json v0.10.2 // indirect
22 | github.com/golang/protobuf v1.5.2 // indirect
23 | github.com/json-iterator/go v1.1.12 // indirect
24 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
25 | github.com/leodido/go-urn v1.4.0 // indirect
26 | github.com/mattn/go-isatty v0.0.20 // indirect
27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
28 | github.com/modern-go/reflect2 v1.0.2 // indirect
29 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
30 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
31 | github.com/ugorji/go/codec v1.2.12 // indirect
32 | golang.org/x/arch v0.8.0 // indirect
33 | golang.org/x/crypto v0.31.0 // indirect
34 | golang.org/x/net v0.25.0 // indirect
35 | golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
36 | golang.org/x/sys v0.28.0 // indirect
37 | golang.org/x/text v0.21.0 // indirect
38 | google.golang.org/appengine v1.6.7 // indirect
39 | google.golang.org/protobuf v1.34.1 // indirect
40 | gopkg.in/yaml.v3 v3.0.1 // indirect
41 | )
42 |
--------------------------------------------------------------------------------
/go/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | go run server.go
--------------------------------------------------------------------------------
/java/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/java/.env.example:
--------------------------------------------------------------------------------
1 | ../.env.example
--------------------------------------------------------------------------------
/java/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | pom.xml.tag
3 | pom.xml.releaseBackup
4 | pom.xml.versionsBackup
5 | pom.xml.next
6 | release.properties
7 | dependency-reduced-pom.xml
8 | buildNumber.properties
9 | .mvn
10 |
11 | #java
12 | .classpath
13 | .project
14 | .settings
15 |
--------------------------------------------------------------------------------
/java/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM maven:openjdk
2 |
3 | WORKDIR /opt/app
4 | COPY . .
5 | WORKDIR /opt/app/java
6 |
7 | RUN mvn clean package
8 |
9 | EXPOSE 8000
10 | ENTRYPOINT ["java"]
11 | CMD ["-jar", "target/quickstart-1.0-SNAPSHOT.jar", "server", "config.yml"]
12 |
--------------------------------------------------------------------------------
/java/config.yml:
--------------------------------------------------------------------------------
1 | logging:
2 | level: INFO
3 | loggers:
4 | com.plaid: DEBUG
5 |
6 | plaid_client_id: ${PLAID_CLIENT_ID}
7 | plaid_secret: ${PLAID_SECRET}
8 | plaid_products: ${PLAID_PRODUCTS}
9 | plaid_country_codes: ${PLAID_COUNTRY_CODES}
10 | plaid_redirect_uri: ${PLAID_REDIRECT_URI:-""}
11 | plaid_env: ${PLAID_ENV:-sandbox}
12 |
13 | server:
14 | application_connectors:
15 | - type: http
16 | port: 8000
17 | admin_connectors:
18 | - type: http
19 | port: 8001
20 | rootPath: '/api'
21 |
--------------------------------------------------------------------------------
/java/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | 3.0.0
9 |
10 | com.plaid
11 | quickstart
12 | 1.0-SNAPSHOT
13 | jar
14 | Quickstart
15 |
16 | 1.8
17 | 1.8
18 | UTF-8
19 | UTF-8
20 | 1.3.2
21 | com.plaid.quickstart.QuickstartApplication
22 |
23 |
24 |
25 |
26 | io.dropwizard
27 | dropwizard-bom
28 | ${dropwizard.version}
29 | pom
30 | import
31 |
32 |
33 |
34 |
35 |
36 | io.dropwizard
37 | dropwizard-core
38 |
39 |
40 | io.dropwizard
41 | dropwizard-assets
42 | ${dropwizard.version}
43 |
44 |
45 | io.dropwizard
46 | dropwizard-views
47 | ${dropwizard.version}
48 |
49 |
50 | io.dropwizard
51 | dropwizard-views-freemarker
52 | ${dropwizard.version}
53 |
54 |
55 | com.plaid
56 | plaid-java
57 | 29.0.0
58 |
59 |
60 | javax.xml.bind
61 | jaxb-api
62 | 2.3.0
63 |
64 |
65 | com.sun.xml.bind
66 | jaxb-core
67 | 2.3.0
68 |
69 |
70 | com.sun.xml.bind
71 | jaxb-impl
72 | 2.3.0
73 |
74 |
75 | com.squareup.okhttp3
76 | okhttp
77 | 4.12.0
78 |
79 |
80 | org.jetbrains.kotlin
81 | kotlin-stdlib
82 | 1.9.25
83 |
84 |
85 |
86 | javax.activation
87 | activation
88 | 1.1.1
89 |
90 |
91 |
92 |
93 |
94 | maven-shade-plugin
95 | 2.4.3
96 |
97 | true
98 |
99 |
100 |
101 | ${mainClass}
102 |
103 |
104 |
105 |
106 |
107 | package
108 |
109 | shade
110 |
111 |
112 |
113 |
114 |
115 | maven-jar-plugin
116 | 3.0.2
117 |
118 |
119 |
120 | true
121 | ${mainClass}
122 |
123 |
124 |
125 |
126 |
127 | maven-compiler-plugin
128 | 3.6.1
129 |
130 | 1.8
131 | 1.8
132 |
133 |
134 |
135 | maven-source-plugin
136 | 3.0.1
137 |
138 |
139 | attach-sources
140 |
141 | jar
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | maven-project-info-reports-plugin
152 | 2.8.1
153 |
154 | false
155 | false
156 |
157 |
158 |
159 | maven-javadoc-plugin
160 | 2.10.3
161 |
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart;
2 |
3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy;
4 | import com.plaid.client.ApiClient;
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.quickstart.resources.AccessTokenResource;
7 | import com.plaid.quickstart.resources.AccountsResource;
8 | import com.plaid.quickstart.resources.AssetsResource;
9 | import com.plaid.quickstart.resources.AuthResource;
10 | import com.plaid.quickstart.resources.BalanceResource;
11 | import com.plaid.quickstart.resources.CraResource;
12 | import com.plaid.quickstart.resources.HoldingsResource;
13 | import com.plaid.quickstart.resources.IdentityResource;
14 | import com.plaid.quickstart.resources.InfoResource;
15 | import com.plaid.quickstart.resources.InvestmentTransactionsResource;
16 | import com.plaid.quickstart.resources.ItemResource;
17 | import com.plaid.quickstart.resources.LinkTokenResource;
18 | import com.plaid.quickstart.resources.LinkTokenWithPaymentResource;
19 | import com.plaid.quickstart.resources.PaymentInitiationResource;
20 | import com.plaid.quickstart.resources.PublicTokenResource;
21 | import com.plaid.quickstart.resources.SignalResource;
22 | import com.plaid.quickstart.resources.StatementsResource;
23 | import com.plaid.quickstart.resources.TransactionsResource;
24 | import com.plaid.quickstart.resources.TransferAuthorizeResource;
25 | import com.plaid.quickstart.resources.TransferCreateResource;
26 | import com.plaid.quickstart.resources.UserTokenResource;
27 | import io.dropwizard.Application;
28 | import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
29 | import io.dropwizard.configuration.SubstitutingSourceProvider;
30 | import io.dropwizard.setup.Bootstrap;
31 | import io.dropwizard.setup.Environment;
32 |
33 | import java.util.Arrays;
34 | import java.util.HashMap;
35 | import java.util.List;
36 |
37 | public class QuickstartApplication extends Application {
38 | // We store the accessToken in memory - in production, store it in a secure
39 | // persistent data store.
40 | public static String accessToken;
41 | public static String userToken;
42 | public static String itemID;
43 | // The paymentId is only relevant for the UK Payment Initiation product.
44 | // We store the paymentId in memory - in production, store it in a secure
45 | // persistent data store.
46 | public static String paymentId;
47 | // The authorizationId is only relevant for Transfer ACH product.
48 | // We store the transferId in memory - in production, store it in a secure
49 | // persistent data store.
50 | public static String authorizationId;
51 | public static String accountId;
52 |
53 | private PlaidApi plaidClient;
54 | private ApiClient apiClient;
55 | public String plaidEnv;
56 |
57 | public static void main(final String[] args) throws Exception {
58 | new QuickstartApplication().run(args);
59 | }
60 |
61 | @Override
62 | public String getName() {
63 | return "Quickstart";
64 | }
65 |
66 | @Override
67 | public void initialize(final Bootstrap bootstrap) {
68 | bootstrap.getObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
69 | bootstrap.setConfigurationSourceProvider(
70 | new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
71 | new EnvironmentVariableSubstitutor(false)
72 | )
73 | );
74 | }
75 |
76 | @Override
77 | public void run(final QuickstartConfiguration configuration,
78 | final Environment environment) {
79 | // or equivalent, depending on which environment you're calling into
80 | switch (configuration.getPlaidEnv()) {
81 | case "sandbox":
82 | plaidEnv = ApiClient.Sandbox;
83 | break;
84 | case "production":
85 | plaidEnv = ApiClient.Production;
86 | break;
87 | default:
88 | plaidEnv = ApiClient.Sandbox;
89 | }
90 | List plaidProducts = Arrays.asList(configuration.getPlaidProducts().split(","));
91 | List countryCodes = Arrays.asList(configuration.getPlaidCountryCodes().split(","));
92 | String plaidClientId = System.getenv("PLAID_CLIENT_ID");
93 | String plaidSecret = System.getenv("PLAID_SECRET");
94 | String redirectUri = null;
95 | if (configuration.getPlaidRedirectUri() != null && configuration.getPlaidRedirectUri().length() > 0) {
96 | redirectUri = configuration.getPlaidRedirectUri();
97 | }
98 |
99 | HashMap apiKeys = new HashMap();
100 | apiKeys.put("clientId", plaidClientId);
101 | apiKeys.put("secret", plaidSecret);
102 | apiKeys.put("plaidVersion", "2020-09-14");
103 | apiClient = new ApiClient(apiKeys);
104 | apiClient.setPlaidAdapter(plaidEnv);
105 |
106 | plaidClient = apiClient.createService(PlaidApi.class);
107 |
108 | environment.jersey().register(new AccessTokenResource(plaidClient, plaidProducts));
109 | environment.jersey().register(new AccountsResource(plaidClient));
110 | environment.jersey().register(new AssetsResource(plaidClient));
111 | environment.jersey().register(new AuthResource(plaidClient));
112 | environment.jersey().register(new BalanceResource(plaidClient));
113 | environment.jersey().register(new HoldingsResource(plaidClient));
114 | environment.jersey().register(new IdentityResource(plaidClient));
115 | environment.jersey().register(new InfoResource(plaidProducts));
116 | environment.jersey().register(new InvestmentTransactionsResource(plaidClient));
117 | environment.jersey().register(new ItemResource(plaidClient));
118 | environment.jersey().register(new LinkTokenResource(plaidClient, plaidProducts, countryCodes, redirectUri));
119 | environment.jersey().register(new LinkTokenWithPaymentResource(plaidClient, plaidProducts, countryCodes, redirectUri));
120 | environment.jersey().register(new PaymentInitiationResource(plaidClient));
121 | environment.jersey().register(new PublicTokenResource(plaidClient));
122 | environment.jersey().register(new SignalResource(plaidClient));
123 | environment.jersey().register(new StatementsResource(plaidClient));
124 | environment.jersey().register(new TransactionsResource(plaidClient));
125 | environment.jersey().register(new TransferAuthorizeResource(plaidClient));
126 | environment.jersey().register(new TransferCreateResource(plaidClient));
127 | environment.jersey().register(new UserTokenResource(plaidClient, plaidProducts));
128 | environment.jersey().register(new CraResource(plaidClient));
129 | }
130 |
131 | protected PlaidApi client() {
132 | return plaidClient;
133 | }
134 |
135 | protected ApiClient apiClient() {
136 | return apiClient;
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/QuickstartConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart;
2 |
3 | import io.dropwizard.Configuration;
4 | import com.fasterxml.jackson.annotation.JsonProperty;
5 | import org.hibernate.validator.constraints.*;
6 |
7 | public class QuickstartConfiguration extends Configuration {
8 | @NotEmpty
9 | private String plaidClientID;
10 |
11 | @NotEmpty
12 | private String plaidSecret;
13 |
14 | @NotEmpty
15 | private String plaidEnv;
16 |
17 | @NotEmpty
18 | private String plaidProducts;
19 |
20 | @NotEmpty
21 | private String plaidCountryCodes;
22 |
23 | // Parameters used for the OAuth redirect Link flow.
24 |
25 | // Set PLAID_REDIRECT_URI to 'http://localhost:3000/'
26 | // The OAuth redirect flow requires an endpoint on the developer's website
27 | // that the bank website should redirect to. You will need to configure
28 | // this redirect URI for your client ID through the Plaid developer dashboard
29 | // at https://dashboard.plaid.com/team/api.
30 | private String plaidRedirectUri;
31 |
32 | @JsonProperty
33 | public String getPlaidClientID() {
34 | return plaidClientID;
35 | }
36 |
37 | @JsonProperty
38 | public String getPlaidSecret() {
39 | return plaidSecret;
40 | }
41 |
42 | @JsonProperty
43 | public String getPlaidEnv() {
44 | return plaidEnv;
45 | }
46 |
47 | @JsonProperty
48 | public String getPlaidProducts() {
49 | return plaidProducts;
50 | }
51 |
52 | @JsonProperty
53 | public String getPlaidCountryCodes() {
54 | return plaidCountryCodes;
55 | }
56 |
57 | @JsonProperty
58 | public String getPlaidRedirectUri() {
59 | return plaidRedirectUri;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.client.model.ItemPublicTokenExchangeRequest;
7 | import com.plaid.client.model.ItemPublicTokenExchangeResponse;
8 | import com.plaid.quickstart.QuickstartApplication;
9 | import com.plaid.client.model.Products;
10 |
11 | import java.util.List;
12 | import java.util.Arrays;
13 | import javax.ws.rs.POST;
14 | import javax.ws.rs.Path;
15 | import javax.ws.rs.Produces;
16 | import javax.ws.rs.FormParam;
17 | import javax.ws.rs.core.MediaType;
18 |
19 | import org.slf4j.Logger;
20 | import org.slf4j.LoggerFactory;
21 |
22 | import retrofit2.Response;
23 |
24 | @Path("/set_access_token")
25 | @Produces(MediaType.APPLICATION_JSON)
26 | public class AccessTokenResource {
27 | private static final Logger LOG = LoggerFactory.getLogger(AccessTokenResource.class);
28 | private final PlaidApi plaidClient;
29 | private final List plaidProducts;
30 |
31 | public AccessTokenResource(PlaidApi plaidClient, List plaidProducts) {
32 | this.plaidClient = plaidClient;
33 | this.plaidProducts = plaidProducts;
34 | }
35 |
36 | @POST
37 | public InfoResource.InfoResponse getAccessToken(@FormParam("public_token") String publicToken)
38 | throws IOException {
39 | ItemPublicTokenExchangeRequest request = new ItemPublicTokenExchangeRequest()
40 | .publicToken(publicToken);
41 |
42 | Response response = plaidClient
43 | .itemPublicTokenExchange(request)
44 | .execute();
45 |
46 | // Ideally, we would store this somewhere more persistent
47 | QuickstartApplication.
48 | accessToken = response.body().getAccessToken();
49 | QuickstartApplication.itemID = response.body().getItemId();
50 | LOG.info("public token: " + publicToken);
51 | LOG.info("access token: " + QuickstartApplication.accessToken);
52 | LOG.info("item ID: " + response.body().getItemId());
53 | return new InfoResource.InfoResponse(Arrays.asList(), QuickstartApplication.accessToken,
54 | QuickstartApplication.itemID);
55 | }
56 |
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.client.model.AccountsGetRequest;
7 | import com.plaid.client.model.AccountsGetResponse;
8 | import com.plaid.quickstart.QuickstartApplication;
9 |
10 | import javax.ws.rs.GET;
11 | import javax.ws.rs.Path;
12 | import javax.ws.rs.Produces;
13 | import javax.ws.rs.core.MediaType;
14 |
15 | import retrofit2.Response;
16 |
17 | @Path("/accounts")
18 | @Produces(MediaType.APPLICATION_JSON)
19 | public class AccountsResource {
20 | private final PlaidApi plaidClient;
21 |
22 | public AccountsResource(PlaidApi plaidClient) {
23 | this.plaidClient = plaidClient;
24 | }
25 |
26 | @GET
27 | public AccountsGetResponse getAccounts() throws IOException {
28 | AccountsGetRequest request = new AccountsGetRequest()
29 | .accessToken(QuickstartApplication.accessToken);
30 |
31 | Response response = plaidClient
32 | .accountsGet(request)
33 | .execute();
34 | return response.body();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.AssetReportCreateRequest;
8 | import com.plaid.client.model.AssetReportCreateResponse;
9 | import com.plaid.client.model.AssetReportGetRequest;
10 | import com.plaid.client.model.AssetReportGetResponse;
11 | import com.plaid.client.model.AssetReportPDFGetRequest;
12 | import com.plaid.quickstart.QuickstartApplication;
13 | import com.plaid.client.model.PlaidError;
14 | import com.plaid.client.model.PlaidErrorType;
15 | import okhttp3.ResponseBody;
16 |
17 | import java.util.List;
18 | import java.util.ArrayList;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 | import java.util.Base64;
22 | import javax.ws.rs.GET;
23 | import javax.ws.rs.Path;
24 | import javax.ws.rs.Produces;
25 | import javax.ws.rs.core.MediaType;
26 |
27 | import retrofit2.Response;
28 | import com.google.gson.Gson;
29 | import jersey.repackaged.com.google.common.base.Throwables;
30 |
31 | @Path("/assets")
32 | @Produces(MediaType.APPLICATION_JSON)
33 | public class AssetsResource {
34 | private final PlaidApi plaidClient;
35 |
36 | public AssetsResource(PlaidApi plaidClient) {
37 | this.plaidClient = plaidClient;
38 | }
39 |
40 | @GET
41 | public Map getAssetReport() throws IOException {
42 | ArrayList accessTokens = new ArrayList<>();
43 | accessTokens.add(QuickstartApplication.accessToken);
44 |
45 | AssetReportCreateRequest assetReportCreateRequest = new AssetReportCreateRequest()
46 | .accessTokens(accessTokens)
47 | .daysRequested(10);
48 |
49 | Response assetReportCreateResponse = plaidClient
50 | .assetReportCreate(assetReportCreateRequest)
51 | .execute();
52 |
53 | String assetReportToken = assetReportCreateResponse.body().getAssetReportToken();
54 | AssetReportGetRequest assetReportGetRequest = new AssetReportGetRequest()
55 | .assetReportToken(assetReportToken);
56 | Response assetReportGetResponse = null;
57 |
58 | //In a real integration, we would wait for a webhook rather than polling like this
59 | for (int i = 0; i < 5; i++){
60 | assetReportGetResponse = plaidClient.assetReportGet(assetReportGetRequest).execute();
61 | if (assetReportGetResponse.isSuccessful()){
62 | break;
63 | } else {
64 | try {
65 | Gson gson = new Gson();
66 | PlaidError error = gson.fromJson(assetReportGetResponse.errorBody().string(), PlaidError.class);
67 | error.getErrorType().equals(PlaidErrorType.ASSET_REPORT_ERROR);
68 | error.getErrorCode().equals("PRODUCT_NOT_READY");
69 | Thread.sleep(5000);
70 | } catch (Exception e) {
71 | throw Throwables.propagate(e);
72 | }
73 | }
74 | }
75 |
76 | AssetReportPDFGetRequest assetReportPDFGetRequest = new AssetReportPDFGetRequest()
77 | .assetReportToken(assetReportToken);
78 |
79 | Response assetReportPDFGetResponse = plaidClient
80 | .assetReportPdfGet(assetReportPDFGetRequest)
81 | .execute();
82 |
83 | String pdf = Base64.getEncoder().encodeToString(assetReportPDFGetResponse.body().bytes());
84 | Map responseMap = new HashMap<>();
85 | responseMap.put("json", assetReportGetResponse.body().getReport());
86 | responseMap.put("pdf", pdf);
87 | return responseMap;
88 |
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/AuthResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.client.model.AuthGetRequest;
7 | import com.plaid.client.model.AuthGetResponse;
8 | import com.plaid.quickstart.QuickstartApplication;
9 |
10 | import javax.ws.rs.GET;
11 | import javax.ws.rs.Path;
12 | import javax.ws.rs.Produces;
13 | import javax.ws.rs.core.MediaType;
14 |
15 | import retrofit2.Response;
16 |
17 | @Path("/auth")
18 | @Produces(MediaType.APPLICATION_JSON)
19 | public class AuthResource {
20 | private final PlaidApi plaidClient;
21 |
22 | public AuthResource(PlaidApi plaidClient) {
23 | this.plaidClient = plaidClient;
24 | }
25 |
26 | @GET
27 | public AuthGetResponse getAccounts() throws IOException {
28 |
29 | AuthGetRequest request = new AuthGetRequest()
30 | .accessToken(QuickstartApplication.accessToken);
31 | Response response = plaidClient
32 | .authGet(request)
33 | .execute();
34 |
35 | return response.body();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.client.model.AccountsBalanceGetRequest;
7 | import com.plaid.client.model.AccountsGetResponse;
8 | import com.plaid.quickstart.QuickstartApplication;
9 |
10 | import javax.ws.rs.GET;
11 | import javax.ws.rs.Path;
12 | import javax.ws.rs.Produces;
13 | import javax.ws.rs.core.MediaType;
14 |
15 | import retrofit2.Response;
16 |
17 | @Path("/balance")
18 | @Produces(MediaType.APPLICATION_JSON)
19 | public class BalanceResource {
20 | private final PlaidApi plaidClient;
21 |
22 | public BalanceResource(PlaidApi plaidClient) {
23 | this.plaidClient = plaidClient;
24 | }
25 |
26 | @GET
27 | public AccountsGetResponse getAccounts() throws IOException {
28 | AccountsBalanceGetRequest request = new AccountsBalanceGetRequest()
29 | .accessToken(QuickstartApplication.accessToken);
30 |
31 | Response response = plaidClient
32 | .accountsBalanceGet(request)
33 | .execute();
34 | return response.body();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/CraResource.java:
--------------------------------------------------------------------------------
1 |
2 | package com.plaid.quickstart.resources;
3 |
4 | import com.plaid.client.model.CraCheckReportBaseReportGetRequest;
5 | import com.plaid.client.model.CraCheckReportBaseReportGetResponse;
6 | import com.plaid.client.model.CraCheckReportIncomeInsightsGetRequest;
7 | import com.plaid.client.model.CraCheckReportIncomeInsightsGetResponse;
8 | import com.plaid.client.model.CraCheckReportPDFGetRequest;
9 | import com.plaid.client.model.CraCheckReportPartnerInsightsGetRequest;
10 | import com.plaid.client.model.CraCheckReportPartnerInsightsGetResponse;
11 | import com.plaid.client.model.CraPDFAddOns;
12 | import com.plaid.client.request.PlaidApi;
13 | import com.plaid.quickstart.QuickstartApplication;
14 |
15 | import jersey.repackaged.com.google.common.base.Throwables;
16 | import okhttp3.ResponseBody;
17 | import retrofit2.Call;
18 | import retrofit2.Response;
19 |
20 | import javax.ws.rs.GET;
21 | import javax.ws.rs.Path;
22 | import javax.ws.rs.Produces;
23 | import javax.ws.rs.core.MediaType;
24 | import java.io.IOException;
25 | import java.util.Base64;
26 | import java.util.HashMap;
27 | import java.util.Map;
28 |
29 | @Produces(MediaType.APPLICATION_JSON)
30 | @Path("/cra")
31 | public class CraResource {
32 | private final PlaidApi plaidClient;
33 |
34 | public CraResource(PlaidApi plaidClient) {
35 | this.plaidClient = plaidClient;
36 | }
37 |
38 | // Retrieve CRA Base Report and PDF
39 | // Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget
40 | // PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget
41 | @GET
42 | @Path("/get_base_report")
43 | public Map getBaseReport() throws IOException {
44 | CraCheckReportBaseReportGetRequest request = new CraCheckReportBaseReportGetRequest();
45 | request.setUserToken(QuickstartApplication.userToken);
46 | CraCheckReportBaseReportGetResponse baseReportResponse = pollWithRetries(
47 | plaidClient.craCheckReportBaseReportGet(request)
48 | ).body();
49 |
50 | CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest();
51 | pdfRequest.setUserToken(QuickstartApplication.userToken);
52 | Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute();
53 |
54 | String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes());
55 |
56 | Map responseMap = new HashMap<>();
57 | responseMap.put("report", baseReportResponse.getReport());
58 | responseMap.put("pdf", pdfBase64);
59 | return responseMap;
60 | }
61 |
62 | // Retrieve CRA Income Insights and PDF with Insights
63 | // Income insights:
64 | // https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget
65 | // PDF w/ income insights:
66 | // https://plaid.com/docs/check/api/#cracheck_reportpdfget
67 | @GET
68 | @Path("/get_income_insights")
69 | public Map getIncomeInsigts() throws IOException {
70 | CraCheckReportIncomeInsightsGetRequest request = new CraCheckReportIncomeInsightsGetRequest();
71 | request.setUserToken(QuickstartApplication.userToken);
72 | CraCheckReportIncomeInsightsGetResponse baseReportResponse = pollWithRetries(
73 | plaidClient.craCheckReportIncomeInsightsGet(request)
74 | ).body();
75 |
76 | CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest();
77 | pdfRequest.setUserToken(QuickstartApplication.userToken);
78 | pdfRequest.addAddOnsItem(CraPDFAddOns.CRA_INCOME_INSIGHTS);
79 | Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute();
80 |
81 | String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes());
82 |
83 | Map responseMap = new HashMap<>();
84 | responseMap.put("report", baseReportResponse.getReport());
85 | responseMap.put("pdf", pdfBase64);
86 | return responseMap;
87 | }
88 |
89 | // Retrieve CRA Partner Insights
90 | // https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget
91 | @GET
92 | @Path("/get_partner_insights")
93 | public CraCheckReportPartnerInsightsGetResponse getPartnerInsigts() throws IOException {
94 | CraCheckReportPartnerInsightsGetRequest request = new CraCheckReportPartnerInsightsGetRequest();
95 | request.setUserToken(QuickstartApplication.userToken);
96 | return pollWithRetries(plaidClient.craCheckReportPartnerInsightsGet(request)).body();
97 | }
98 |
99 | // Since this quickstart does not support webhooks, this function can be used to
100 | // poll
101 | // an API that would otherwise be triggered by a webhook.
102 | // For a webhook example, see
103 | // https://github.com/plaid/tutorial-resources or
104 | // https://github.com/plaid/pattern
105 | private Response pollWithRetries(Call requestCallback) throws IOException {
106 | for (int i = 0; i <= 20; i++) {
107 | Response response = requestCallback.execute();
108 |
109 | if (response.isSuccessful()) {
110 | return response;
111 | } else {
112 | try {
113 | Thread.sleep(5000);
114 | } catch (Exception e) {
115 | throw Throwables.propagate(e);
116 | }
117 | }
118 |
119 | }
120 | throw Throwables.propagate(new Exception("Ran out of retries while polling"));
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 |
7 | import com.plaid.client.request.PlaidApi;
8 | import com.plaid.client.model.InvestmentsHoldingsGetRequest;
9 | import com.plaid.client.model.InvestmentsHoldingsGetResponse;
10 | import com.plaid.quickstart.QuickstartApplication;
11 |
12 | import javax.ws.rs.GET;
13 | import javax.ws.rs.Path;
14 | import javax.ws.rs.Produces;
15 | import javax.ws.rs.core.MediaType;
16 |
17 | import retrofit2.Response;
18 |
19 | @Path("/holdings")
20 | @Produces(MediaType.APPLICATION_JSON)
21 | public class HoldingsResource {
22 | private final PlaidApi plaidClient;
23 |
24 | public HoldingsResource(PlaidApi plaidClient) {
25 | this.plaidClient = plaidClient;
26 | }
27 |
28 | @GET
29 | public HoldingsResponse getAccounts() throws IOException {
30 |
31 | InvestmentsHoldingsGetRequest request = new InvestmentsHoldingsGetRequest()
32 | .accessToken(QuickstartApplication.accessToken);
33 |
34 | Response response = plaidClient
35 | .investmentsHoldingsGet(request)
36 | .execute();
37 |
38 | return new HoldingsResponse(response.body());
39 | }
40 |
41 | private static class HoldingsResponse {
42 | @JsonProperty
43 | private final InvestmentsHoldingsGetResponse holdings;
44 |
45 | public HoldingsResponse(InvestmentsHoldingsGetResponse response) {
46 | this.holdings = response;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.AccountIdentity;
8 | import com.plaid.client.model.IdentityGetRequest;
9 | import com.plaid.client.model.IdentityGetResponse;
10 | import com.plaid.quickstart.QuickstartApplication;
11 |
12 | import java.util.List;
13 | import javax.ws.rs.GET;
14 | import javax.ws.rs.Path;
15 | import javax.ws.rs.Produces;
16 | import javax.ws.rs.core.MediaType;
17 |
18 | import retrofit2.Response;
19 |
20 | @Path("/identity")
21 | @Produces(MediaType.APPLICATION_JSON)
22 | public class IdentityResource {
23 | private final PlaidApi plaidClient;
24 |
25 | public IdentityResource(PlaidApi plaidClient) {
26 | this.plaidClient = plaidClient;
27 | }
28 |
29 | @GET
30 | public IdentityResponse getAccounts() throws IOException {
31 | IdentityGetRequest request = new IdentityGetRequest()
32 | .accessToken(QuickstartApplication.accessToken);
33 | Response response = plaidClient
34 | .identityGet(request)
35 | .execute();
36 | return new IdentityResponse(response.body());
37 | }
38 |
39 | private static class IdentityResponse {
40 | @JsonProperty
41 | private final List identity;
42 |
43 | public IdentityResponse(IdentityGetResponse response) {
44 | this.identity = response.getAccounts();
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/InfoResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 |
5 | import com.plaid.quickstart.QuickstartApplication;
6 |
7 | import java.util.List;
8 | import javax.ws.rs.POST;
9 | import javax.ws.rs.Path;
10 | import javax.ws.rs.Produces;
11 | import javax.ws.rs.core.MediaType;
12 |
13 | @Path("/info")
14 | @Produces(MediaType.APPLICATION_JSON)
15 | public class InfoResource {
16 | private final List plaidProducts;
17 |
18 | public InfoResource(List plaidProducts) {
19 | this.plaidProducts = plaidProducts;
20 | }
21 |
22 | public static class InfoResponse {
23 | @JsonProperty
24 | private final String itemId;
25 | @JsonProperty
26 | private final String accessToken;
27 | @JsonProperty
28 | private final List products;
29 |
30 | public InfoResponse(List plaidProducts, String accessToken, String itemID) {
31 | this.products = plaidProducts;
32 | this.accessToken = accessToken;
33 | this.itemId = itemID;
34 | }
35 | }
36 |
37 | @POST
38 | public InfoResponse getInfo() {
39 | return new InfoResponse(plaidProducts, QuickstartApplication.accessToken,
40 | QuickstartApplication.itemID);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 |
5 |
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.InvestmentsTransactionsGetRequest;
8 | import com.plaid.client.model.InvestmentsTransactionsGetResponse;
9 | import com.plaid.client.model.InvestmentsTransactionsGetRequestOptions;
10 | import com.plaid.quickstart.QuickstartApplication;
11 |
12 | import java.io.IOException;
13 | import java.time.LocalDate;
14 | import javax.ws.rs.GET;
15 | import javax.ws.rs.Path;
16 | import javax.ws.rs.Produces;
17 | import javax.ws.rs.core.MediaType;
18 |
19 | import retrofit2.Response;
20 |
21 | @Path("/investments_transactions")
22 | @Produces(MediaType.APPLICATION_JSON)
23 | public class InvestmentTransactionsResource {
24 | private final PlaidApi plaidClient;
25 |
26 | public InvestmentTransactionsResource(PlaidApi plaidClient) {
27 | this.plaidClient = plaidClient;
28 | }
29 |
30 | @GET
31 | public InvestmentTransactionsResponse getAccounts() throws IOException {
32 | LocalDate startDate = LocalDate.now().minusDays(30);
33 | LocalDate endDate = LocalDate.now();
34 | InvestmentsTransactionsGetRequestOptions options = new InvestmentsTransactionsGetRequestOptions()
35 | .count(100);
36 |
37 | InvestmentsTransactionsGetRequest request = new InvestmentsTransactionsGetRequest()
38 | .accessToken(QuickstartApplication.accessToken)
39 | .startDate(startDate)
40 | .endDate(endDate)
41 | .options(options);
42 |
43 | Response response = plaidClient
44 | .investmentsTransactionsGet(request)
45 | .execute();
46 | return new InvestmentTransactionsResponse(response.body());
47 | }
48 |
49 | private static class InvestmentTransactionsResponse {
50 | @JsonProperty
51 | private InvestmentsTransactionsGetResponse investmentsTransactions;
52 |
53 | public InvestmentTransactionsResponse(InvestmentsTransactionsGetResponse investmentTransactions) {
54 | this.investmentsTransactions = investmentTransactions;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/ItemResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 |
7 | import com.plaid.client.request.PlaidApi;
8 | import com.plaid.client.model.CountryCode;
9 | import com.plaid.client.model.ItemGetRequest;
10 | import com.plaid.client.model.ItemGetResponse;
11 | import com.plaid.client.model.InstitutionsGetByIdRequest;
12 | import com.plaid.client.model.InstitutionsGetByIdResponse;
13 | import com.plaid.client.model.Institution;
14 | import com.plaid.client.model.ItemWithConsentFields;
15 | import com.plaid.quickstart.QuickstartApplication;
16 |
17 | import javax.ws.rs.GET;
18 | import javax.ws.rs.Path;
19 | import javax.ws.rs.Produces;
20 | import javax.ws.rs.core.MediaType;
21 |
22 | import retrofit2.Response;
23 |
24 | @Path("/item")
25 | @Produces(MediaType.APPLICATION_JSON)
26 | public class ItemResource {
27 | private final PlaidApi plaidClient;
28 |
29 | public ItemResource(PlaidApi plaidClient) {
30 | this.plaidClient = plaidClient;
31 | }
32 |
33 | @GET
34 | public ItemResponse getItem() throws IOException {
35 | ItemGetRequest request = new ItemGetRequest()
36 | .accessToken(QuickstartApplication.accessToken);
37 |
38 | Response itemResponse = plaidClient
39 | .itemGet(request)
40 | .execute();
41 |
42 | InstitutionsGetByIdRequest institutionsRequest = new InstitutionsGetByIdRequest()
43 | .institutionId(itemResponse.body().getItem().getInstitutionId())
44 | .addCountryCodesItem(CountryCode.US);
45 |
46 | Response institutionsResponse = plaidClient
47 | .institutionsGetById(institutionsRequest)
48 | .execute();
49 |
50 | return new ItemResponse(
51 | itemResponse.body().getItem(),
52 | institutionsResponse.body().getInstitution()
53 | );
54 | }
55 |
56 | public static class ItemResponse {
57 | @JsonProperty
58 | public ItemWithConsentFields item;
59 |
60 | @JsonProperty
61 | public Institution institution;
62 |
63 | public ItemResponse(ItemWithConsentFields item, Institution institution) {
64 | this.item = item;
65 | this.institution = institution;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import com.plaid.client.model.ConsumerReportPermissiblePurpose;
5 | import com.plaid.client.model.CountryCode;
6 | import com.plaid.client.model.LinkTokenCreateRequest;
7 | import com.plaid.client.model.LinkTokenCreateRequestCraOptions;
8 | import com.plaid.client.model.LinkTokenCreateRequestStatements;
9 | import com.plaid.client.model.LinkTokenCreateRequestUser;
10 | import com.plaid.client.model.LinkTokenCreateResponse;
11 | import com.plaid.client.model.Products;
12 | import com.plaid.client.request.PlaidApi;
13 | import com.plaid.quickstart.QuickstartApplication;
14 | import retrofit2.Response;
15 |
16 | import javax.ws.rs.POST;
17 | import javax.ws.rs.Path;
18 | import javax.ws.rs.Produces;
19 | import javax.ws.rs.core.MediaType;
20 | import java.io.IOException;
21 | import java.time.LocalDate;
22 | import java.util.ArrayList;
23 | import java.util.Arrays;
24 | import java.util.Date;
25 | import java.util.List;
26 |
27 | @Path("/create_link_token")
28 | @Produces(MediaType.APPLICATION_JSON)
29 | public class LinkTokenResource {
30 | private final PlaidApi plaidClient;
31 | private final List plaidProducts;
32 | private final List countryCodes;
33 | private final String redirectUri;
34 | private final List correctedPlaidProducts;
35 | private final List correctedCountryCodes;
36 |
37 | public LinkTokenResource(PlaidApi plaidClient, List plaidProducts,
38 | List countryCodes, String redirectUri) {
39 | this.plaidClient = plaidClient;
40 | this.plaidProducts = plaidProducts;
41 | this.countryCodes = countryCodes;
42 | this.redirectUri = redirectUri;
43 | this.correctedPlaidProducts = new ArrayList<>();
44 | this.correctedCountryCodes = new ArrayList<>();
45 | }
46 |
47 | public static class LinkToken {
48 | @JsonProperty
49 | private String linkToken;
50 |
51 |
52 | public LinkToken(String linkToken) {
53 | this.linkToken = linkToken;
54 | }
55 | }
56 |
57 | @POST public LinkToken getLinkToken() throws IOException {
58 |
59 |
60 | String clientUserId = Long.toString((new Date()).getTime());
61 | LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser()
62 | .clientUserId(clientUserId);
63 |
64 | for (int i = 0; i < this.plaidProducts.size(); i++){
65 | this.correctedPlaidProducts.add(Products.fromValue(this.plaidProducts.get(i)));
66 | };
67 |
68 | for (int i = 0; i < this.countryCodes.size(); i++){
69 | this.correctedCountryCodes.add(CountryCode.fromValue(this.countryCodes.get(i)));
70 | };
71 |
72 |
73 | LinkTokenCreateRequest request = new LinkTokenCreateRequest()
74 | .user(user)
75 | .clientName("Quickstart Client")
76 | .products(this.correctedPlaidProducts)
77 | .countryCodes(this.correctedCountryCodes)
78 | .language("en")
79 | .redirectUri(this.redirectUri);
80 |
81 | if (this.correctedPlaidProducts.contains(Products.STATEMENTS)) {
82 | LinkTokenCreateRequestStatements statementsConfig = new LinkTokenCreateRequestStatements()
83 | .startDate(LocalDate.now().minusDays(30))
84 | .endDate(LocalDate.now());
85 | request.setStatements(statementsConfig);
86 | }
87 |
88 | if (plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) {
89 | request.userToken(QuickstartApplication.userToken);
90 | request.consumerReportPermissiblePurpose(ConsumerReportPermissiblePurpose.ACCOUNT_REVIEW_CREDIT);
91 | LinkTokenCreateRequestCraOptions options = new LinkTokenCreateRequestCraOptions();
92 | options.daysRequested(60);
93 | request.craOptions(options);
94 | }
95 |
96 | Response response =plaidClient
97 | .linkTokenCreate(request)
98 | .execute();
99 | return new LinkToken(response.body().getLinkToken());
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.plaid.client.request.PlaidApi;
6 | import com.plaid.client.model.PaymentAmount;
7 | import com.plaid.client.model.PaymentAmountCurrency;
8 | import com.plaid.client.model.Products;
9 | import com.plaid.client.model.CountryCode;
10 | import com.plaid.client.model.PaymentInitiationPaymentCreateRequest;
11 | import com.plaid.client.model.PaymentInitiationPaymentCreateResponse;
12 | import com.plaid.client.model.LinkTokenCreateRequest;
13 | import com.plaid.client.model.LinkTokenCreateRequestUser;
14 | import com.plaid.client.model.LinkTokenCreateResponse;
15 | import com.plaid.client.model.PaymentInitiationRecipientCreateResponse;
16 | import com.plaid.client.model.PaymentInitiationAddress;
17 | import com.plaid.client.model.PaymentInitiationRecipientCreateRequest;
18 | import com.plaid.client.model.LinkTokenCreateRequestPaymentInitiation;
19 | import com.plaid.quickstart.QuickstartApplication;
20 |
21 | import java.util.Arrays;
22 | import java.util.List;
23 | import java.util.Date;
24 |
25 | import javax.ws.rs.POST;
26 | import javax.ws.rs.Path;
27 | import javax.ws.rs.Produces;
28 | import javax.ws.rs.core.MediaType;
29 |
30 | import retrofit2.Response;
31 |
32 | @Path("/create_link_token_for_payment")
33 | @Produces(MediaType.APPLICATION_JSON)
34 | public class LinkTokenWithPaymentResource {
35 | private final PlaidApi plaidClient;
36 | private final String redirectUri;
37 |
38 | public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProducts,
39 | List countryCodes, String redirectUri) {
40 | this.plaidClient = plaidClient;
41 | this.redirectUri = redirectUri;
42 | }
43 |
44 | @POST public LinkTokenResource.LinkToken getLinkToken() throws IOException {
45 |
46 | PaymentInitiationAddress address = new PaymentInitiationAddress()
47 | .street(Arrays.asList("Street Name 999"))
48 | .city("City")
49 | .postalCode("99999")
50 | .country("GB");
51 |
52 | PaymentInitiationRecipientCreateRequest recipientCreateRequest = new PaymentInitiationRecipientCreateRequest()
53 | .name("Jonathan Doe")
54 | .iban("GB33BUKB20201555555555")
55 | .address(address);
56 |
57 |
58 | Response recipientResponse =
59 | this.plaidClient
60 | .paymentInitiationRecipientCreate(recipientCreateRequest)
61 | .execute();
62 |
63 |
64 | String recipientId = recipientResponse.body().getRecipientId();
65 |
66 | PaymentAmount amount = new PaymentAmount()
67 | .currency(PaymentAmountCurrency.GBP)
68 | .value(999.99);
69 |
70 | PaymentInitiationPaymentCreateRequest paymentCreateRequest = new PaymentInitiationPaymentCreateRequest()
71 | .recipientId(recipientId)
72 | .reference("reference")
73 | .amount(amount);
74 |
75 | Response paymentResponse = plaidClient
76 | .paymentInitiationPaymentCreate(paymentCreateRequest)
77 | .execute();
78 |
79 |
80 | String paymentId = paymentResponse.body().getPaymentId();
81 | QuickstartApplication.paymentId = paymentId;
82 |
83 | LinkTokenCreateRequestPaymentInitiation paymentInitiation = new LinkTokenCreateRequestPaymentInitiation()
84 | .paymentId(paymentId);
85 |
86 | // This should correspond to a unique id for the current user.
87 | // Typically, this will be a user ID number from your application.
88 | // Personally identifiable information, such as an email address or phone number, should not be used here.
89 | String clientUserId = Long.toString((new Date()).getTime());
90 | LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser()
91 | .clientUserId(clientUserId);
92 |
93 | LinkTokenCreateRequest request = new LinkTokenCreateRequest()
94 | .user(user)
95 | .clientName("Quickstart Client")
96 | // The 'payment_initiation' product has to be the only element in the 'products' list.
97 | .products(Arrays.asList(Products.PAYMENT_INITIATION))
98 | // Institutions from all listed countries will be shown.
99 | .countryCodes(Arrays.asList(CountryCode.GB))
100 | .language("en")
101 | .redirectUri(this.redirectUri)
102 | .paymentInitiation(paymentInitiation);
103 |
104 | Response response =plaidClient
105 | .linkTokenCreate(request)
106 | .execute();
107 |
108 | return new LinkTokenResource.LinkToken(response.body().getLinkToken());
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.google.gson.Gson;
7 |
8 | import com.plaid.client.request.PlaidApi;
9 | import com.plaid.client.model.PaymentInitiationPaymentGetRequest;
10 | import com.plaid.client.model.PaymentInitiationPaymentGetResponse;
11 | import com.plaid.quickstart.QuickstartApplication;
12 |
13 | import javax.ws.rs.GET;
14 | import javax.ws.rs.Path;
15 | import javax.ws.rs.Produces;
16 | import javax.ws.rs.core.MediaType;
17 |
18 | import org.slf4j.Logger;
19 | import org.slf4j.LoggerFactory;
20 |
21 | import retrofit2.Response;
22 |
23 | // This functionality is only relevant for the UK Payment Initiation product.
24 | @Path("/payment")
25 | @Produces(MediaType.APPLICATION_JSON)
26 | public class PaymentInitiationResource {
27 | private static final Logger LOG = LoggerFactory.getLogger(PaymentInitiationResource.class);
28 |
29 | private final PlaidApi plaidClient;
30 |
31 | public PaymentInitiationResource(PlaidApi plaidClient) {
32 | this.plaidClient = plaidClient;
33 | }
34 |
35 | @GET
36 | public PaymentResponse getPayment() throws IOException {
37 | String paymentId = QuickstartApplication.paymentId;
38 |
39 | PaymentInitiationPaymentGetRequest request = new PaymentInitiationPaymentGetRequest()
40 | .paymentId(paymentId);
41 |
42 | Response response =
43 | plaidClient
44 | .paymentInitiationPaymentGet(request)
45 | .execute();
46 | if (!response.isSuccessful()) {
47 | try {
48 | Gson gson = new Gson();
49 | Error errorResponse = gson.fromJson(response.errorBody().string(), Error.class);
50 | LOG.error("error: " + errorResponse);
51 | } catch (Exception e) {
52 | LOG.error("error", e);
53 | }
54 | }
55 | return new PaymentResponse(response.body());
56 | }
57 |
58 | private static class PaymentResponse {
59 | @JsonProperty
60 | private final PaymentInitiationPaymentGetResponse payment;
61 |
62 | public PaymentResponse(PaymentInitiationPaymentGetResponse response) {
63 | this.payment = response;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 |
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.ItemPublicTokenCreateRequest;
8 | import com.plaid.client.model.ItemPublicTokenCreateResponse;
9 | import com.plaid.quickstart.QuickstartApplication;
10 |
11 | import javax.ws.rs.GET;
12 | import javax.ws.rs.Path;
13 | import javax.ws.rs.Produces;
14 | import javax.ws.rs.core.MediaType;
15 |
16 | import retrofit2.Response;
17 |
18 | @Path("/create_public_token")
19 | @Produces(MediaType.APPLICATION_JSON)
20 | public class PublicTokenResource {
21 | private final PlaidApi plaidClient;
22 |
23 | public PublicTokenResource(PlaidApi plaidClient) {
24 | this.plaidClient = plaidClient;
25 | }
26 |
27 | @GET
28 | public ItemPublicTokenCreateResponse createPublicToken() throws IOException {
29 |
30 | ItemPublicTokenCreateRequest request = new ItemPublicTokenCreateRequest()
31 | .accessToken(QuickstartApplication.accessToken);
32 |
33 | Response response = plaidClient
34 | .itemCreatePublicToken(request)
35 | .execute();
36 |
37 | return response.body();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/SignalResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.AccountsGetRequest;
8 | import com.plaid.client.model.AccountsGetResponse;
9 | import com.plaid.client.model.AccountIdentity;
10 | import com.plaid.client.model.SignalEvaluateRequest;
11 | import com.plaid.client.model.SignalEvaluateResponse;
12 | import com.plaid.quickstart.QuickstartApplication;
13 |
14 | import java.util.List;
15 | import javax.ws.rs.GET;
16 | import javax.ws.rs.Path;
17 | import javax.ws.rs.Produces;
18 | import javax.ws.rs.core.MediaType;
19 |
20 | import retrofit2.Response;
21 |
22 | @Path("/signal_evaluate")
23 | @Produces(MediaType.APPLICATION_JSON)
24 | public class SignalResource {
25 | private final PlaidApi plaidClient;
26 |
27 | public SignalResource(PlaidApi plaidClient) {
28 | this.plaidClient = plaidClient;
29 | }
30 |
31 | @GET
32 | public SignalEvaluateResponse signalEvaluate() throws IOException {
33 | AccountsGetRequest accountsGetRequest = new AccountsGetRequest()
34 | .accessToken(QuickstartApplication.accessToken);
35 |
36 | Response accountsGetResponse = plaidClient
37 | .accountsGet(accountsGetRequest)
38 | .execute();
39 |
40 | QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId();
41 |
42 | SignalEvaluateRequest signalEvaluateRequest = new SignalEvaluateRequest()
43 | .accessToken(QuickstartApplication.accessToken)
44 | .accountId(QuickstartApplication.accountId)
45 | .clientTransactionId("txn1234")
46 | .amount(100.00);
47 |
48 | Response signalEvaluateResponse = plaidClient
49 | .signalEvaluate(signalEvaluateRequest)
50 | .execute();
51 |
52 | return signalEvaluateResponse.body();
53 |
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.StatementsListRequest;
8 | import com.plaid.client.model.StatementsListResponse;
9 | import com.plaid.client.model.StatementsDownloadRequest;
10 | import com.plaid.quickstart.QuickstartApplication;
11 | import okhttp3.ResponseBody;
12 |
13 | import java.util.List;
14 | import java.util.HashMap;
15 | import java.util.Map;
16 | import java.util.Base64;
17 | import javax.ws.rs.GET;
18 | import javax.ws.rs.Path;
19 | import javax.ws.rs.Produces;
20 | import javax.ws.rs.core.MediaType;
21 |
22 | import retrofit2.Response;
23 |
24 | @Path("/statements")
25 | @Produces(MediaType.APPLICATION_JSON)
26 | public class StatementsResource {
27 | private final PlaidApi plaidClient;
28 |
29 | public StatementsResource(PlaidApi plaidClient) {
30 | this.plaidClient = plaidClient;
31 | }
32 |
33 | @GET
34 | public Map statementsList() throws IOException {
35 |
36 | StatementsListRequest statementsListRequest = new StatementsListRequest()
37 | .accessToken(QuickstartApplication.accessToken);
38 |
39 | Response statementsListResponse = plaidClient
40 | .statementsList(statementsListRequest)
41 | .execute();
42 |
43 | StatementsDownloadRequest statementsDownloadRequest = new StatementsDownloadRequest()
44 | .accessToken(QuickstartApplication.accessToken)
45 | .statementId(statementsListResponse.body().getAccounts().get(0).getStatements().get(0).getStatementId());
46 |
47 | Response statementsDownloadResponse = plaidClient
48 | .statementsDownload(statementsDownloadRequest)
49 | .execute();
50 |
51 | String pdf = Base64.getEncoder().encodeToString(statementsDownloadResponse.body().bytes());
52 |
53 | Map responseMap = new HashMap<>();
54 | responseMap.put("json", statementsListResponse.body());
55 | responseMap.put("pdf", pdf);
56 |
57 | return responseMap;
58 |
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 | import java.util.ArrayList;
5 | import java.util.Comparator;
6 | import java.util.List;
7 |
8 | import com.fasterxml.jackson.annotation.JsonProperty;
9 |
10 | import com.plaid.client.request.PlaidApi;
11 | import com.plaid.client.model.TransactionsSyncRequest;
12 | import com.plaid.client.model.TransactionsSyncResponse;
13 | import com.plaid.client.model.Transaction;
14 | import com.plaid.client.model.RemovedTransaction;
15 | import com.plaid.quickstart.QuickstartApplication;
16 |
17 | import javax.ws.rs.GET;
18 | import javax.ws.rs.Path;
19 | import javax.ws.rs.Produces;
20 | import javax.ws.rs.core.MediaType;
21 |
22 | import retrofit2.Response;
23 |
24 | @Path("/transactions")
25 | @Produces(MediaType.APPLICATION_JSON)
26 | public class TransactionsResource {
27 | private final PlaidApi plaidClient;
28 |
29 |
30 | public TransactionsResource(PlaidApi plaidClient) {
31 | this.plaidClient = plaidClient;
32 | }
33 |
34 | @GET
35 | public TransactionsResponse getTransactions() throws IOException, InterruptedException {
36 | // Set cursor to empty to receive all historical updates
37 | String cursor = null;
38 |
39 | // New transaction updates since "cursor"
40 | List added = new ArrayList();
41 | List modified = new ArrayList();
42 | List removed = new ArrayList();
43 | boolean hasMore = true;
44 | // Iterate through each page of new transaction updates for item
45 | while (hasMore) {
46 | TransactionsSyncRequest request = new TransactionsSyncRequest()
47 | .accessToken(QuickstartApplication.accessToken)
48 | .cursor(cursor);
49 |
50 | Response response = plaidClient.transactionsSync(request).execute();
51 | TransactionsSyncResponse responseBody = response.body();
52 |
53 | cursor = responseBody.getNextCursor();
54 |
55 | // If no transactions are available yet, wait and poll the endpoint.
56 | // Normally, we would listen for a webhook, but the Quickstart doesn't
57 | // support webhooks. For a webhook example, see
58 | // https://github.com/plaid/tutorial-resources or
59 | // https://github.com/plaid/pattern
60 |
61 | if (cursor.equals("")) {
62 | Thread.sleep(2000);
63 | continue;
64 | }
65 | // Add this page of results
66 | added.addAll(responseBody.getAdded());
67 | modified.addAll(responseBody.getModified());
68 | removed.addAll(responseBody.getRemoved());
69 | hasMore = responseBody.getHasMore();
70 | }
71 |
72 | // Return the 8 most recent transactions
73 | added.sort(new TransactionsResource.CompareTransactionDate());
74 | List latestTransactions = added.subList(Math.max(added.size() - 8, 0), added.size());
75 | return new TransactionsResponse(latestTransactions);
76 | }
77 |
78 | private class CompareTransactionDate implements Comparator {
79 | @Override
80 | public int compare(Transaction o1, Transaction o2) {
81 | return o1.getDate().compareTo(o2.getDate());
82 | }
83 | }
84 |
85 | private static class TransactionsResponse {
86 | @JsonProperty
87 | private final List latest_transactions;
88 |
89 | public TransactionsResponse(List latestTransactions) {
90 | this.latest_transactions = latestTransactions;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.Transfer;
8 | import com.plaid.quickstart.QuickstartApplication;
9 | import com.plaid.client.model.TransferAuthorizationUserInRequest;
10 | import com.plaid.client.model.TransferAuthorizationCreateRequest;
11 | import com.plaid.client.model.TransferAuthorizationCreateResponse;
12 | import com.plaid.client.model.TransferType;
13 | import com.plaid.client.model.TransferNetwork;
14 | import com.plaid.client.model.ACHClass;
15 | import com.plaid.client.model.AccountsGetRequest;
16 | import com.plaid.client.model.AccountsGetResponse;
17 |
18 | import java.util.List;
19 | import javax.ws.rs.GET;
20 | import javax.ws.rs.Path;
21 | import javax.ws.rs.Produces;
22 | import javax.ws.rs.core.MediaType;
23 |
24 | import org.slf4j.Logger;
25 | import org.slf4j.LoggerFactory;
26 |
27 | import retrofit2.Response;
28 |
29 | @Path("/transfer_authorize")
30 | @Produces(MediaType.APPLICATION_JSON)
31 | public class TransferAuthorizeResource {
32 | private final PlaidApi plaidClient;
33 |
34 | public TransferAuthorizeResource(PlaidApi plaidClient) {
35 | this.plaidClient = plaidClient;
36 | }
37 |
38 | @GET
39 | public TransferAuthorizationCreateResponse authorizeTransfer() throws IOException {
40 | AccountsGetRequest accountsGetRequest = new AccountsGetRequest()
41 | .accessToken(QuickstartApplication.accessToken);
42 |
43 | Response accountsGetResponse = plaidClient
44 | .accountsGet(accountsGetRequest)
45 | .execute();
46 |
47 | QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId();
48 |
49 | TransferAuthorizationUserInRequest user = new TransferAuthorizationUserInRequest()
50 | .legalName("FirstName LastName");
51 |
52 | TransferAuthorizationCreateRequest transferAuthorizationCreateRequest = new TransferAuthorizationCreateRequest()
53 | .accessToken(QuickstartApplication.accessToken)
54 | .accountId(QuickstartApplication.accountId)
55 | .type(TransferType.DEBIT)
56 | .network(TransferNetwork.ACH)
57 | .amount("1.00")
58 | .achClass(ACHClass.PPD)
59 | .user(user);
60 |
61 | Response transferAuthorizationCreateResponse = plaidClient
62 | .transferAuthorizationCreate(transferAuthorizationCreateRequest)
63 | .execute();
64 |
65 | QuickstartApplication.authorizationId = transferAuthorizationCreateResponse.body().getAuthorization().getId();
66 | return transferAuthorizationCreateResponse.body();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import java.io.IOException;
4 |
5 | import com.fasterxml.jackson.annotation.JsonProperty;
6 | import com.plaid.client.request.PlaidApi;
7 | import com.plaid.client.model.Transfer;
8 | import com.plaid.quickstart.QuickstartApplication;
9 | import com.plaid.client.model.TransferCreateRequest;
10 | import com.plaid.client.model.TransferCreateResponse;
11 |
12 | import java.util.List;
13 | import javax.ws.rs.GET;
14 | import javax.ws.rs.Path;
15 | import javax.ws.rs.Produces;
16 | import javax.ws.rs.core.MediaType;
17 |
18 | import org.slf4j.Logger;
19 | import org.slf4j.LoggerFactory;
20 |
21 | import retrofit2.Response;
22 |
23 | @Path("/transfer_create")
24 | @Produces(MediaType.APPLICATION_JSON)
25 | public class TransferCreateResource {
26 | private final PlaidApi plaidClient;
27 |
28 | public TransferCreateResource(PlaidApi plaidClient) {
29 | this.plaidClient = plaidClient;
30 | }
31 |
32 | @GET
33 | public TransferCreateResponse createTransfer() throws IOException {
34 | TransferCreateRequest request = new TransferCreateRequest()
35 | .authorizationId(QuickstartApplication.authorizationId)
36 | .accessToken(QuickstartApplication.accessToken)
37 | .accountId(QuickstartApplication.accountId)
38 | .description("Debit");
39 | Response transferCreateResponse = plaidClient
40 | .transferCreate(request)
41 | .execute();
42 | return transferCreateResponse.body();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java:
--------------------------------------------------------------------------------
1 | package com.plaid.quickstart.resources;
2 |
3 | import com.plaid.client.model.AddressData;
4 | import com.plaid.client.model.ConsumerReportUserIdentity;
5 | import com.plaid.client.model.UserCreateRequest;
6 | import com.plaid.client.model.UserCreateResponse;
7 | import com.plaid.client.request.PlaidApi;
8 | import com.plaid.quickstart.QuickstartApplication;
9 | import retrofit2.Response;
10 |
11 | import javax.ws.rs.POST;
12 | import javax.ws.rs.Path;
13 | import javax.ws.rs.Produces;
14 | import javax.ws.rs.core.MediaType;
15 | import java.io.IOException;
16 | import java.util.Arrays;
17 | import java.util.List;
18 | import java.util.UUID;
19 |
20 | @Path("/create_user_token")
21 | @Produces(MediaType.APPLICATION_JSON)
22 | public class UserTokenResource {
23 | private final PlaidApi plaidClient;
24 | private final List plaidProducts;
25 |
26 | public UserTokenResource(PlaidApi plaidClient, List plaidProducts) {
27 | this.plaidClient = plaidClient;
28 | this.plaidProducts = plaidProducts;
29 | }
30 |
31 | // Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows
32 | // https://plaid.com/docs/api/users/#usercreate
33 | @POST
34 | public UserCreateResponse createUserToken() throws IOException {
35 |
36 | UserCreateRequest userCreateRequest = new UserCreateRequest()
37 | // Typically, this will be a user ID number from your application.
38 | .clientUserId("user_" + UUID.randomUUID());
39 |
40 | if (plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) {
41 | AddressData addressData = new AddressData()
42 | .city("New York")
43 | .region("NY")
44 | .street("4 Priver Drive")
45 | .postalCode("11111")
46 | .country("US");
47 | userCreateRequest.consumerReportUserIdentity(new ConsumerReportUserIdentity()
48 | .firstName("Harry")
49 | .lastName("Potter")
50 | .phoneNumbers(Arrays.asList("+16174567890"))
51 | .emails(List.of("harrypotter@example.com"))
52 | .primaryAddress(addressData));
53 | }
54 | Response userResponse = plaidClient.userCreate(userCreateRequest).execute();
55 |
56 | // Ideally, we would store this somewhere more persistent
57 | QuickstartApplication.userToken = userResponse.body().getUserToken();
58 | return userResponse.body();
59 | }
60 | }
--------------------------------------------------------------------------------
/java/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use the env variables in .env to start the server
3 | env $(cat .env | grep -v "#" | xargs) java -jar target/quickstart-1.0-SNAPSHOT.jar server config.yml
4 |
--------------------------------------------------------------------------------
/node/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/node/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/node/.env.example:
--------------------------------------------------------------------------------
1 | ../.env.example
--------------------------------------------------------------------------------
/node/.gitignore:
--------------------------------------------------------------------------------
1 | .prettierrc
--------------------------------------------------------------------------------
/node/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.com
2 |
--------------------------------------------------------------------------------
/node/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.1
2 |
--------------------------------------------------------------------------------
/node/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "requirePragma": false,
3 | "jsxSingleQuote": true,
4 | "singleQuote": true,
5 | "trailingComma": "all",
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "semi": true
9 | }
10 |
--------------------------------------------------------------------------------
/node/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 |
3 | WORKDIR /opt/app
4 | RUN chown -R node:node /opt/app
5 | COPY --chown=node:node ./node/package*.json /opt/app/
6 |
7 | USER node
8 | RUN npm install
9 |
10 | COPY --chown=node:node ./node/index.js ./
11 | COPY --chown=node:node ./.env ./
12 |
13 | EXPOSE 8000
14 | ENTRYPOINT ["node"]
15 | CMD ["index.js"]
16 |
--------------------------------------------------------------------------------
/node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plaid-node-walkthrough",
3 | "version": "0.1.0",
4 | "description": "A sample app and accompanying walkthrough for the Plaid API.",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "watch": "nodemon index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "developers@plaid.com",
12 | "repository": "https://github.com/plaid/quickstart",
13 | "license": "ISC",
14 | "dependencies": {
15 | "body-parser": "^1.20.3",
16 | "cors": "^2.8.5",
17 | "dotenv": "^8.2.0",
18 | "ejs": "^3.1.10",
19 | "express": "^4.21.1",
20 | "moment": "^2.30.1",
21 | "nodemon": "^3.1.7",
22 | "plaid": "^30.0.0",
23 | "uuid": "^9.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/node/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | node index.js
--------------------------------------------------------------------------------
/python/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/python/.env.example:
--------------------------------------------------------------------------------
1 | ../.env.example
--------------------------------------------------------------------------------
/python/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:alpine
2 |
3 | WORKDIR /opt/app
4 | COPY . .
5 | WORKDIR /opt/app/python
6 |
7 | RUN pip3 install -r requirements.txt
8 |
9 | ENV FLASK_APP=/opt/app/python/server.py
10 | EXPOSE 8000
11 | ENTRYPOINT ["python"]
12 | CMD ["-m", "flask", "run", "--host=0.0.0.0", "--port=8000"]
13 |
--------------------------------------------------------------------------------
/python/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==3.0.0
2 | plaid_python==28.0.0
3 | python-dotenv==0.15.0
4 | itsdangerous==2.1.2
5 | werkzeug==3.1.3
6 |
--------------------------------------------------------------------------------
/python/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | python server.py || python3 server.py
--------------------------------------------------------------------------------
/ruby/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/ruby/.env.example:
--------------------------------------------------------------------------------
1 | ../.env.example
--------------------------------------------------------------------------------
/ruby/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | .bundle
3 |
--------------------------------------------------------------------------------
/ruby/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.2.2
2 |
--------------------------------------------------------------------------------
/ruby/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:3.0
2 |
3 | WORKDIR /opt/app
4 | COPY . .
5 | WORKDIR /opt/app/ruby
6 |
7 | RUN bundle update --bundler
8 | RUN bundle install
9 |
10 | EXPOSE 8000
11 | ENTRYPOINT ["ruby"]
12 | CMD ["app.rb", "-o", "0.0.0.0"]
13 |
--------------------------------------------------------------------------------
/ruby/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'sinatra'
4 |
5 | gem 'http'
6 |
7 | gem "dotenv", "~> 2.7"
8 |
9 | gem "plaid", "= 34.0.0"
10 |
11 | gem "webrick"
12 |
13 | gem "rackup", "~> 2.2"
14 | gem "puma", "~> 6.5"
15 |
--------------------------------------------------------------------------------
/ruby/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.7)
5 | public_suffix (>= 2.0.2, < 7.0)
6 | base64 (0.2.0)
7 | domain_name (0.6.20240107)
8 | dotenv (2.8.1)
9 | faraday (2.12.1)
10 | faraday-net_http (>= 2.0, < 3.5)
11 | json
12 | logger
13 | faraday-multipart (1.0.4)
14 | multipart-post (~> 2)
15 | faraday-net_http (3.4.0)
16 | net-http (>= 0.5.0)
17 | ffi (1.17.0)
18 | ffi-compiler (1.3.2)
19 | ffi (>= 1.15.5)
20 | rake
21 | http (5.2.0)
22 | addressable (~> 2.8)
23 | base64 (~> 0.1)
24 | http-cookie (~> 1.0)
25 | http-form_data (~> 2.2)
26 | llhttp-ffi (~> 0.5.0)
27 | http-cookie (1.0.7)
28 | domain_name (~> 0.5)
29 | http-form_data (2.3.0)
30 | json (2.8.2)
31 | llhttp-ffi (0.5.0)
32 | ffi-compiler (~> 1.0)
33 | rake (~> 13.0)
34 | logger (1.6.1)
35 | multipart-post (2.4.1)
36 | mustermann (3.0.3)
37 | ruby2_keywords (~> 0.0.1)
38 | net-http (0.5.0)
39 | uri
40 | nio4r (2.7.4)
41 | plaid (34.0.0)
42 | faraday (>= 1.0.1, < 3.0)
43 | faraday-multipart (>= 1.0.1, < 2.0)
44 | public_suffix (6.0.1)
45 | puma (6.5.0)
46 | nio4r (~> 2.0)
47 | rack (3.1.8)
48 | rack-protection (4.1.1)
49 | base64 (>= 0.1.0)
50 | logger (>= 1.6.0)
51 | rack (>= 3.0.0, < 4)
52 | rack-session (2.0.0)
53 | rack (>= 3.0.0)
54 | rackup (2.2.1)
55 | rack (>= 3)
56 | rake (13.2.1)
57 | ruby2_keywords (0.0.5)
58 | sinatra (4.1.1)
59 | logger (>= 1.6.0)
60 | mustermann (~> 3.0)
61 | rack (>= 3.0.0, < 4)
62 | rack-protection (= 4.1.1)
63 | rack-session (>= 2.0.0, < 3)
64 | tilt (~> 2.0)
65 | tilt (2.4.0)
66 | uri (1.0.2)
67 | webrick (1.9.0)
68 |
69 | PLATFORMS
70 | ruby
71 |
72 | DEPENDENCIES
73 | dotenv (~> 2.7)
74 | http
75 | plaid (= 34.0.0)
76 | puma (~> 6.5)
77 | rackup (~> 2.2)
78 | sinatra
79 | webrick
80 |
81 | BUNDLED WITH
82 | 2.4.10
83 |
--------------------------------------------------------------------------------
/ruby/app.rb:
--------------------------------------------------------------------------------
1 | # Load env vars from .env file
2 | require 'dotenv'
3 | Dotenv.load
4 |
5 | require 'base64'
6 | require 'date'
7 | require 'json'
8 | require 'plaid'
9 | require 'sinatra'
10 |
11 | set :port, ENV['APP_PORT'] || 8000
12 |
13 | # disable CSRF warning and Rack protection on localhost due to usage of local /api proxy in react app.
14 | # delete this for a production application.
15 | set :protection, :except => [:json_csrf]
16 | set :host_authorization, { permitted_hosts: [] }
17 |
18 | configuration = Plaid::Configuration.new
19 | configuration.server_index = Plaid::Configuration::Environment[ENV['PLAID_ENV'] || 'sandbox']
20 | configuration.api_key['PLAID-CLIENT-ID'] = ENV['PLAID_CLIENT_ID']
21 | configuration.api_key['PLAID-SECRET'] = ENV['PLAID_SECRET']
22 | configuration.api_key['Plaid-Version'] = '2020-09-14'
23 |
24 | api_client = Plaid::ApiClient.new(
25 | configuration
26 | )
27 |
28 | client = Plaid::PlaidApi.new(api_client)
29 | products = ENV['PLAID_PRODUCTS'].split(',')
30 | # We store the access_token and user_token in memory - in production, store it in a secure
31 | # persistent data store.
32 | access_token = nil
33 | user_token = nil
34 | # The payment_id is only relevant for the UK Payment Initiation product.
35 | # We store the payment_token in memory - in production, store it in a secure
36 | # persistent data store.
37 |
38 | payment_id = nil
39 | item_id = nil
40 |
41 | # The authorization_id is only relevant for Transfer ACH product.
42 | # We store the authorization_id in memory - in production, store it in a secure
43 | # persistent data store.
44 | authorization_id = nil
45 | account_id = nil
46 |
47 | post '/api/info' do
48 | content_type :json
49 | {
50 | item_id: item_id,
51 | access_token: access_token,
52 | products: ENV['PLAID_PRODUCTS'].split(','),
53 | }.to_json
54 | end
55 |
56 | # Exchange token flow - exchange a Link public_token for
57 | # an API access_token
58 | # https://plaid.com/docs/#exchange-token-flow
59 | post '/api/set_access_token' do
60 | item_public_token_exchange_request = Plaid::ItemPublicTokenExchangeRequest.new(
61 | { public_token: params['public_token'] }
62 | )
63 | exchange_token_response =
64 | client.item_public_token_exchange(
65 | item_public_token_exchange_request
66 | )
67 | access_token = exchange_token_response.access_token
68 | item_id = exchange_token_response.item_id
69 | pretty_print_response(exchange_token_response.to_hash)
70 | content_type :json
71 | exchange_token_response.to_hash.to_json
72 | end
73 |
74 | # Retrieve Transactions for an Item
75 | # https://plaid.com/docs/#transactions
76 | get '/api/transactions' do
77 | begin
78 | # Set cursor to empty to receive all historical updates
79 | cursor = ''
80 |
81 | # New transaction updates since "cursor"
82 | added = []
83 | modified = []
84 | removed = [] # Removed transaction ids
85 | has_more = true
86 | # Iterate through each page of new transaction updates for item
87 | while has_more
88 | request = Plaid::TransactionsSyncRequest.new(
89 | {
90 | access_token: access_token,
91 | cursor: cursor
92 | }
93 | )
94 | response = client.transactions_sync(request)
95 | cursor = response.next_cursor
96 |
97 | # If no transactions are available yet, wait and poll the endpoint.
98 | # Normally, we would listen for a webhook but the Quickstart doesn't
99 | # support webhooks. For a webhook example, see
100 | # https://github.com/plaid/tutorial-resources or
101 | # https://github.com/plaid/pattern
102 | if cursor == ""
103 | sleep 2
104 | next
105 | end
106 |
107 | # Add this page of results
108 | added += response.added
109 | modified += response.modified
110 | removed += response.removed
111 | has_more = response.has_more
112 | pretty_print_response(response.to_hash)
113 | end
114 | # Return the 8 most recent transactions
115 | content_type :json
116 | { latest_transactions: added.sort_by(&:date).last(8).map(&:to_hash) }.to_json
117 | rescue Plaid::ApiError => e
118 | error_response = format_error(e)
119 | pretty_print_response(error_response)
120 | content_type :json
121 | error_response.to_json
122 | end
123 | end
124 |
125 | # Retrieve ACH or ETF account numbers for an Item
126 | # https://plaid.com/docs/#auth
127 | get '/api/auth' do
128 | begin
129 | auth_get_request = Plaid::AuthGetRequest.new({ access_token: access_token })
130 | auth_response = client.auth_get(auth_get_request)
131 | pretty_print_response(auth_response.to_hash)
132 | content_type :json
133 | auth_response.to_hash.to_json
134 | rescue Plaid::ApiError => e
135 | error_response = format_error(e)
136 | pretty_print_response(error_response)
137 | content_type :json
138 | error_response.to_json
139 | end
140 | end
141 |
142 | # Retrieve Identity data for an Item
143 | # https://plaid.com/docs/#identity
144 | get '/api/identity' do
145 | begin
146 | identity_get_request = Plaid::IdentityGetRequest.new({ access_token: access_token })
147 | identity_response = client.identity_get(identity_get_request)
148 | pretty_print_response(identity_response.to_hash)
149 | content_type :json
150 | { identity: identity_response.to_hash[:accounts] }.to_json
151 | rescue Plaid::ApiError => e
152 | error_response = format_error(e)
153 | pretty_print_response(error_response)
154 | content_type :json
155 | error_response.to_json
156 | end
157 | end
158 |
159 | # Retrieve real-time balance data for each of an Item's accounts
160 | # https://plaid.com/docs/#balance
161 | get '/api/balance' do
162 | begin
163 | balance_get_request = Plaid::AccountsBalanceGetRequest.new({ access_token: access_token })
164 | balance_response = client.accounts_balance_get(balance_get_request)
165 | pretty_print_response(balance_response.to_hash)
166 | content_type :json
167 | balance_response.to_hash.to_json
168 | rescue Plaid::ApiError => e
169 | error_response = format_error(e)
170 | pretty_print_response(error_response)
171 | content_type :json
172 | error_response.to_json
173 | end
174 | end
175 |
176 | # Retrieve an Item's accounts
177 | # https://plaid.com/docs/#accounts
178 | get '/api/accounts' do
179 | begin
180 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
181 | account_response = client.accounts_get(accounts_get_request)
182 | pretty_print_response(account_response.to_hash)
183 | content_type :json
184 | account_response.to_hash.to_json
185 | rescue Plaid::ApiError => e
186 | error_response = format_error(e)
187 | pretty_print_response(error_response)
188 | content_type :json
189 | error_response.to_json
190 | end
191 | end
192 |
193 | # Retrieve Holdings data for an Item
194 | # https://plaid.com/docs/#investments
195 | get '/api/holdings' do
196 | begin
197 | investments_holdings_get_request = Plaid::InvestmentsHoldingsGetRequest.new({ access_token: access_token })
198 | product_response = client.investments_holdings_get(investments_holdings_get_request)
199 | pretty_print_response(product_response.to_hash)
200 | content_type :json
201 | { holdings: product_response.to_hash }.to_json
202 | rescue Plaid::ApiError => e
203 | error_response = format_error(e)
204 | pretty_print_response(error_response)
205 | content_type :json
206 | error_response.to_hash.to_json
207 | end
208 | end
209 |
210 | # Retrieve Investment Transactions for an Item
211 | # https://plaid.com/docs/#investments
212 | get '/api/investments_transactions' do
213 | begin
214 | start_date = (Date.today - 30)
215 | end_date = Date.today
216 | investments_transactions_get_request = Plaid::InvestmentsTransactionsGetRequest.new(
217 | {
218 | access_token: access_token,
219 | start_date: start_date,
220 | end_date: end_date
221 | }
222 | )
223 | transactions_response = client.investments_transactions_get(investments_transactions_get_request)
224 | pretty_print_response(transactions_response.to_hash)
225 | content_type :json
226 | { investments_transactions: transactions_response.to_hash }.to_json
227 | rescue Plaid::ApiError => e
228 | error_response = format_error(e)
229 | pretty_print_response(error_response)
230 | content_type :json
231 | error_response.to_json
232 | end
233 | end
234 |
235 | # Create and then retrieve an Asset Report for one or more Items. Note that an
236 | # Asset Report can contain up to 100 items, but for simplicity we're only
237 | # including one Item here.
238 | # https://plaid.com/docs/#assets
239 | # rubocop:disable Metrics/BlockLength
240 | get '/api/assets' do
241 | begin
242 | options = {
243 | client_report_id: '123',
244 | webhook: 'https://www.example.com',
245 | user: {
246 | client_user_id: '789',
247 | first_name: 'Jane',
248 | middle_name: 'Leah',
249 | last_name: 'Doe',
250 | ssn: '123-45-6789',
251 | phone_number: '(555) 123-4567',
252 | email: 'jane.doe@example.com'
253 | }
254 | }
255 | asset_report_create_request = Plaid::AssetReportCreateRequest.new(
256 | {
257 | access_tokens: [access_token],
258 | days_requested: 20,
259 | options: options
260 | }
261 | )
262 | asset_report_create_response =
263 | client.asset_report_create(asset_report_create_request)
264 | pretty_print_response(asset_report_create_response.to_hash)
265 | rescue Plaid::ApiError => e
266 | error_response = format_error(e)
267 | pretty_print_response(error_response)
268 | content_type :json
269 | error_response.to_json
270 | end
271 |
272 | asset_report_token = asset_report_create_response.asset_report_token
273 | asset_report_json = nil
274 | num_retries_remaining = 20
275 | while num_retries_remaining.positive?
276 | begin
277 | asset_report_get_request = Plaid::AssetReportGetRequest.new({ asset_report_token: asset_report_token })
278 | asset_report_get_response = client.asset_report_get(asset_report_get_request)
279 | asset_report_json = asset_report_get_response.report
280 | break
281 | rescue Plaid::ApiError => e
282 | json_response = JSON.parse(e.response_body)
283 | if json_response['error_code'] == 'PRODUCT_NOT_READY'
284 | num_retries_remaining -= 1
285 | sleep(1)
286 | next
287 | end
288 | error_response = format_error(e)
289 | pretty_print_response(error_response)
290 | content_type :json
291 | return error_response.to_json
292 | end
293 | end
294 |
295 | if asset_report_json.nil?
296 | content_type :json
297 | return {
298 | error: {
299 | error_code: 0,
300 | error_message: 'Timed out when polling for Asset Report'
301 | }
302 | }.to_json
303 | end
304 |
305 | asset_report_pdf_get_request = Plaid::AssetReportPDFGetRequest.new({ asset_report_token: asset_report_token })
306 | asset_report_pdf = client.asset_report_pdf_get( asset_report_pdf_get_request)
307 |
308 | content_type :json
309 | { json: asset_report_json.to_hash,
310 | pdf: Base64.encode64(File.read(asset_report_pdf)) }.to_json
311 | end
312 |
313 | get '/api/statements' do
314 | begin
315 | statements_list_request = Plaid::StatementsListRequest.new(
316 | {
317 | access_token: access_token
318 | }
319 | )
320 | statements_list_response =
321 | client.statements_list(statements_list_request)
322 | pretty_print_response(statements_list_response.to_hash)
323 | rescue Plaid::ApiError => e
324 | error_response = format_error(e)
325 | pretty_print_response(error_response)
326 | content_type :json
327 | error_response.to_json
328 | end
329 | statement_id = statements_list_response.accounts[0].statements[0].statement_id
330 | statements_download_request = Plaid::StatementsDownloadRequest.new({ access_token: access_token, statement_id: statement_id })
331 | statement_pdf = client.statements_download(statements_download_request)
332 |
333 | content_type :json
334 | { json: statements_list_response.to_hash,
335 | pdf: Base64.encode64(File.read(statement_pdf)) }.to_json
336 | end
337 |
338 | # rubocop:enable Metrics/BlockLength
339 |
340 | # Retrieve high-level information about an Item
341 | # https://plaid.com/docs/#retrieve-item
342 | get '/api/item' do
343 | begin
344 | item_get_request = Plaid::ItemGetRequest.new({ access_token: access_token})
345 | item_response = client.item_get(item_get_request)
346 | institutions_get_by_id_request = Plaid::InstitutionsGetByIdRequest.new(
347 | {
348 | institution_id: item_response.item.institution_id,
349 | country_codes: ['US']
350 | }
351 | )
352 | institution_response =
353 | client.institutions_get_by_id(institutions_get_by_id_request)
354 | pretty_print_response(item_response.to_hash)
355 | pretty_print_response(institution_response.to_hash)
356 | content_type :json
357 | { item: item_response.item.to_hash,
358 | institution: institution_response.institution.to_hash }.to_json
359 | rescue Plaid::ApiError => e
360 | error_response = format_error(e)
361 | pretty_print_response(error_response)
362 | content_type :json
363 | error_response.to_json
364 | end
365 | end
366 |
367 | # This functionality is only relevant for the ACH Transfer product.
368 | # Retrieve Transfer for a specified Transfer ID
369 |
370 | get '/api/transfer_authorize' do
371 | begin
372 | # We call /accounts/get to obtain first account_id - in production,
373 | # account_id's should be persisted in a data store and retrieved
374 | # from there.
375 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
376 | accounts_get_response = client.accounts_get(accounts_get_request)
377 | account_id = accounts_get_response.accounts[0].account_id
378 |
379 | transfer_authorization_create_request = Plaid::TransferAuthorizationCreateRequest.new({
380 | access_token: access_token,
381 | account_id: account_id,
382 | type: 'debit',
383 | network: 'ach',
384 | amount: '1.00',
385 | ach_class: 'ppd',
386 | user: {
387 | legal_name: 'FirstName LastName',
388 | email_address: 'foobar@email.com',
389 | address: {
390 | street: '123 Main St.',
391 | city: 'San Francisco',
392 | region: 'CA',
393 | postal_code: '94053',
394 | country: 'US'
395 | }
396 | },
397 | })
398 | transfer_authorization_create_response = client.transfer_authorization_create(transfer_authorization_create_request)
399 | pretty_print_response(transfer_authorization_create_response.to_hash)
400 | authorization_id = transfer_authorization_create_response.authorization.id
401 | content_type :json
402 | transfer_authorization_create_response.to_hash.to_json
403 | rescue Plaid::ApiError => e
404 | error_response = format_error(e)
405 | pretty_print_response(error_response)
406 | content_type :json
407 | error_response.to_json
408 | end
409 | end
410 |
411 | get '/api/signal_evaluate' do
412 | begin
413 | # We call /accounts/get to obtain first account_id - in production,
414 | # account_id's should be persisted in a data store and retrieved
415 | # from there.
416 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
417 | accounts_get_response = client.accounts_get(accounts_get_request)
418 | account_id = accounts_get_response.accounts[0].account_id
419 |
420 | signal_evaluate_request = Plaid::SignalEvaluateRequest.new({
421 | access_token: access_token,
422 | account_id: account_id,
423 | client_transaction_id: 'tx1234',
424 | amount: 100.00
425 | })
426 | signal_evaluate_response = client.signal_evaluate(signal_evaluate_request)
427 | pretty_print_response(signal_evaluate_response.to_hash)
428 | content_type :json
429 | signal_evaluate_response.to_hash.to_json
430 | rescue Plaid::ApiError => e
431 | error_response = format_error(e)
432 | pretty_print_response(error_response)
433 | content_type :json
434 | error_response.to_json
435 | end
436 | end
437 |
438 | get '/api/transfer_create' do
439 | begin
440 | transfer_create_request = Plaid::TransferCreateRequest.new({
441 | access_token: access_token,
442 | account_id: account_id,
443 | authorization_id: authorization_id,
444 | description: 'Debit'
445 | })
446 | transfer_create_response = client.transfer_create(transfer_create_request)
447 | pretty_print_response(transfer_create_response.to_hash)
448 | content_type :json
449 | transfer_create_response.to_hash.to_json
450 | rescue Plaid::ApiError => e
451 | error_response = format_error(e)
452 | pretty_print_response(error_response)
453 | content_type :json
454 | error_response.to_json
455 | end
456 | end
457 |
458 | # This functionality is only relevant for the UK Payment Initiation product.
459 | # Retrieve Payment for a specified Payment ID
460 | get '/api/payment' do
461 | begin
462 | payment_initiation_payment_get_request = Plaid::PaymentInitiationPaymentGetRequest.new({ payment_id: payment_id})
463 | payment_get_response = client.payment_initiation_payment_get(payment_initiation_payment_get_request)
464 | pretty_print_response(payment_get_response.to_hash)
465 | content_type :json
466 | { payment: payment_get_response.to_hash}.to_json
467 | rescue Plaid::ApiError => e
468 | error_response = format_error(e)
469 | pretty_print_response(error_response)
470 | content_type :json
471 | error_response.to_json
472 | end
473 | end
474 |
475 | post '/api/create_link_token' do
476 | begin
477 | link_token_create_request = Plaid::LinkTokenCreateRequest.new(
478 | {
479 | user: { client_user_id: 'user-id' },
480 | client_name: 'Plaid Quickstart',
481 | products: ENV['PLAID_PRODUCTS'].split(','),
482 | country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
483 | language: 'en',
484 | redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
485 | }
486 | )
487 | if ENV['PLAID_PRODUCTS'].split(',').include?("statements")
488 | today = Date.today
489 | statements = Plaid::LinkTokenCreateRequestStatements.new(
490 | end_date: today,
491 | start_date: today-30
492 | )
493 | link_token_create_request.statements=statements
494 | end
495 | if products.any? { |product| product.start_with?("cra_") }
496 | link_token_create_request.cra_options = Plaid::LinkTokenCreateRequestCraOptions.new(
497 | days_requested: 60
498 | )
499 | link_token_create_request.user_token=user_token
500 | link_token_create_request.consumer_report_permissible_purpose =Plaid::ConsumerReportPermissiblePurpose::ACCOUNT_REVIEW_CREDIT
501 |
502 | end
503 | link_response = client.link_token_create(link_token_create_request)
504 | pretty_print_response(link_response.to_hash)
505 | content_type :json
506 | { link_token: link_response.link_token }.to_json
507 | rescue Plaid::ApiError => e
508 | error_response = format_error(e)
509 | pretty_print_response(error_response)
510 | content_type :json
511 | error_response.to_json
512 | end
513 | end
514 |
515 | # Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows
516 | # https://plaid.com/docs/api/users/#usercreate
517 | post '/api/create_user_token' do
518 | begin
519 | request_data = {
520 | # Typically this will be a user ID number from your application.
521 | client_user_id: 'user_' + SecureRandom.uuid
522 | }
523 |
524 | if products.any? { |product| product.start_with?("cra_") }
525 | request_data[:consumer_report_user_identity] = {
526 | first_name: 'Harry',
527 | last_name: 'Potter',
528 | phone_numbers: ['+16174567890'],
529 | emails: ['harrypotter@example.com'],
530 | primary_address: {
531 | city: 'New York',
532 | region: 'NY',
533 | street: '4 Privet Drive',
534 | postal_code: '11111',
535 | country: 'US'
536 | }
537 | }
538 | end
539 |
540 | user = client.user_create(Plaid::UserCreateRequest.new(request_data))
541 | user_token = user.user_token
542 | content_type :json
543 | user.to_hash.to_json
544 | rescue Plaid::ApiError => e
545 | error_response = format_error(e)
546 | pretty_print_response(error_response)
547 | content_type :json
548 | error_response.to_json
549 | end
550 | end
551 |
552 | def nil_if_empty_envvar(field)
553 | val = ENV[field]
554 | puts "val #{val}"
555 | if !val.nil? && val.length > 0
556 | return val
557 | else
558 | return nil
559 | end
560 | end
561 |
562 | # This functionality is only relevant for the UK/EU Payment Initiation product.
563 | # Sets the payment token in memory on the server side. We generate a new
564 | # payment token so that the developer is not required to supply one.
565 | # This makes the quickstart easier to use.
566 | # See:
567 | # - https://plaid.com/docs/payment-initiation/
568 | # - https://plaid.com/docs/#payment-initiation-create-link-token-request
569 | post '/api/create_link_token_for_payment' do
570 | begin
571 | payment_initiation_recipient_create_request = Plaid::PaymentInitiationRecipientCreateRequest.new(
572 | {
573 | name: 'Bruce Wayne',
574 | iban: 'GB33BUKB20201555555555',
575 | address: {
576 | street: ['686 Bat Cave Lane'],
577 | city: 'Gotham',
578 | postal_code: '99999',
579 | country: 'GB',
580 | },
581 | bacs: {
582 | account: '26207729',
583 | sort_code: '560029',
584 | }
585 | }
586 | )
587 | create_recipient_response = client.payment_initiation_recipient_create(
588 | payment_initiation_recipient_create_request
589 | )
590 | recipient_id = create_recipient_response.recipient_id
591 |
592 | payment_initiation_recipient_get_request = Plaid::PaymentInitiationRecipientGetRequest.new(
593 | {
594 | recipient_id: recipient_id
595 | }
596 | )
597 | get_recipient_response = client.payment_initiation_recipient_get(
598 | payment_initiation_recipient_get_request
599 | )
600 |
601 | payment_initiation_payment_create_request = Plaid::PaymentInitiationPaymentCreateRequest.new(
602 | {
603 | recipient_id: recipient_id,
604 | reference: 'testpayment',
605 | amount: {
606 | value: 100.00,
607 | currency: 'GBP'
608 | }
609 | }
610 | )
611 | create_payment_response = client.payment_initiation_payment_create(
612 | payment_initiation_payment_create_request
613 | )
614 | payment_id = create_payment_response.payment_id
615 |
616 | link_token_create_request = Plaid::LinkTokenCreateRequest.new(
617 | {
618 | client_name: 'Plaid Quickstart',
619 | user: {
620 | # This should correspond to a unique id for the current user.
621 | # Typically, this will be a user ID number from your application.
622 | # Personally identifiable information, such as an email address or phone number, should not be used here.
623 | client_user_id: 'user-id'
624 | },
625 |
626 | # Institutions from all listed countries will be shown.
627 | country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
628 | language: 'en',
629 |
630 | # The 'payment_initiation' product has to be the only element in the 'products' list.
631 | products: ['payment_initiation'],
632 |
633 | payment_initiation: {
634 | payment_id: payment_id
635 | },
636 | redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
637 | }
638 | )
639 | link_response = client.link_token_create(link_token_create_request)
640 | pretty_print_response(link_response.to_hash)
641 | content_type :json
642 | { link_token: link_response.link_token }.to_hash.to_json
643 |
644 | rescue Plaid::ApiError => e
645 | error_response = format_error(e)
646 | pretty_print_response(error_response)
647 | content_type :json
648 | error_response.to_json
649 | end
650 | end
651 |
652 | # Retrieve CRA Base Report and PDF
653 | # Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget
654 | # PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget
655 | get '/api/cra/get_base_report' do
656 | begin
657 | get_response = get_cra_base_report_with_retries(client, user_token)
658 | pretty_print_response(get_response.to_hash)
659 |
660 | pdf_response = client.cra_check_report_pdf_get(
661 | Plaid::CraCheckReportPDFGetRequest.new({ user_token: user_token })
662 | )
663 |
664 | content_type :json
665 | {
666 | report: get_response.report.to_hash,
667 | pdf: Base64.encode64(File.read(pdf_response))
668 | }.to_json
669 | rescue Plaid::ApiError => e
670 | error_response = format_error(e)
671 | pretty_print_response(error_response)
672 | content_type :json
673 | error_response.to_json
674 | end
675 | end
676 |
677 | def get_cra_base_report_with_retries(plaid_client, user_token)
678 | poll_with_retries do
679 | plaid_client.cra_check_report_base_report_get(
680 | Plaid::CraCheckReportBaseReportGetRequest.new({ user_token: user_token })
681 | )
682 | end
683 | end
684 |
685 | # Retrieve CRA Income Insights and PDF with Insights
686 | # Income insights: https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget
687 | # PDF w/ income insights: https://plaid.com/docs/check/api/#cracheck_reportpdfget
688 | get '/api/cra/get_income_insights' do
689 | begin
690 | get_response = get_income_insights_with_retries(client, user_token)
691 | pretty_print_response(get_response.to_hash)
692 |
693 | pdf_response = client.cra_check_report_pdf_get(
694 | Plaid::CraCheckReportPDFGetRequest.new({ user_token: user_token, add_ons: [Plaid::CraPDFAddOns::CRA_INCOME_INSIGHTS] })
695 | )
696 |
697 | content_type :json
698 | {
699 | report: get_response.report.to_hash,
700 | pdf: Base64.encode64(File.read(pdf_response))
701 | }.to_json
702 | rescue Plaid::ApiError => e
703 | error_response = format_error(e)
704 | pretty_print_response(error_response)
705 | content_type :json
706 | error_response.to_json
707 | end
708 | end
709 |
710 | def get_income_insights_with_retries(plaid_client, user_token)
711 | poll_with_retries do
712 | plaid_client.cra_check_report_income_insights_get(
713 | Plaid::CraCheckReportIncomeInsightsGetRequest.new({ user_token: user_token })
714 | )
715 | end
716 | end
717 |
718 | # Retrieve CRA Partner Insights
719 | # https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget
720 | get '/api/cra/get_partner_insights' do
721 | begin
722 | response = get_check_partner_insights_with_retries(client, user_token)
723 | pretty_print_response(response.to_hash)
724 |
725 | content_type :json
726 | response.to_hash.to_json
727 | rescue Plaid::ApiError => e
728 | error_response = format_error(e)
729 | pretty_print_response(error_response)
730 | content_type :json
731 | error_response.to_json
732 | end
733 | end
734 |
735 | def get_check_partner_insights_with_retries(plaid_client, user_token)
736 | poll_with_retries do
737 | plaid_client.cra_check_report_partner_insights_get(
738 | Plaid::CraCheckReportPartnerInsightsGetRequest.new({ user_token: user_token })
739 | )
740 | end
741 | end
742 |
743 | # Since this quickstart does not support webhooks, this function can be used to poll
744 | # an API that would otherwise be triggered by a webhook.
745 | # For a webhook example, see
746 | # https://github.com/plaid/tutorial-resources or
747 | # https://github.com/plaid/pattern
748 | def poll_with_retries(ms = 1000, retries_left = 20)
749 | begin
750 | yield
751 | rescue Plaid::ApiError => e
752 | if retries_left > 0
753 | sleep(ms / 1000.0)
754 | poll_with_retries(ms, retries_left - 1) { yield }
755 | else
756 | raise 'Ran out of retries while polling'
757 | end
758 | end
759 | end
760 |
761 | def format_error(err)
762 | body = JSON.parse(err.response_body)
763 | {
764 | error: {
765 | status_code: err.code,
766 | error_code: body['error_code'],
767 | error_message: body['error_message'],
768 | error_type: body['error_type']
769 | }
770 | }
771 | end
772 |
773 | def pretty_print_response(response)
774 | puts JSON.pretty_generate(response)
775 | end
776 |
--------------------------------------------------------------------------------
/ruby/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | bundle exec ruby app.rb
--------------------------------------------------------------------------------