├── auth
├── finished
│ ├── .npmrc
│ ├── user_files
│ │ └── note.txt
│ ├── package.json
│ ├── .env.template
│ └── public
│ │ ├── js
│ │ ├── connect.js
│ │ └── utilities.js
│ │ └── index.html
├── start
│ ├── .npmrc
│ ├── user_files
│ │ └── note.txt
│ ├── package.json
│ ├── .env.template
│ └── public
│ │ ├── js
│ │ ├── connect.js
│ │ ├── utilities.js
│ │ └── client.js
│ │ └── index.html
└── package-lock.json
├── transactions
├── start
│ ├── .npmrc
│ ├── database
│ │ └── note.txt
│ ├── server
│ │ ├── utils.js
│ │ ├── plaid.js
│ │ ├── routes
│ │ │ ├── banks.js
│ │ │ ├── debug.js
│ │ │ ├── transactions.js
│ │ │ ├── users.js
│ │ │ └── tokens.js
│ │ ├── simpleTransactionObject.js
│ │ ├── server.js
│ │ └── webhookServer.js
│ ├── .env.template
│ ├── package.json
│ ├── testData.js
│ └── public
│ │ ├── js
│ │ ├── link.js
│ │ ├── signin.js
│ │ ├── utils.js
│ │ └── client.js
│ │ └── index.html
├── finished
│ ├── .npmrc
│ ├── database
│ │ └── note.txt
│ ├── server
│ │ ├── utils.js
│ │ ├── plaid.js
│ │ ├── routes
│ │ │ ├── banks.js
│ │ │ ├── debug.js
│ │ │ ├── users.js
│ │ │ └── tokens.js
│ │ ├── simpleTransactionObject.js
│ │ ├── server.js
│ │ └── webhookServer.js
│ ├── .env.template
│ ├── package.json
│ ├── testData.js
│ └── public
│ │ ├── js
│ │ ├── link.js
│ │ ├── signin.js
│ │ ├── utils.js
│ │ └── client.js
│ │ └── index.html
└── README.md
├── webhooks
├── finished
│ ├── .npmrc
│ ├── .env.template
│ ├── package.json
│ └── public
│ │ ├── js
│ │ ├── connect.js
│ │ └── index.js
│ │ └── index.html
├── start
│ ├── .npmrc
│ ├── .env.template
│ ├── package.json
│ └── public
│ │ ├── js
│ │ ├── connect.js
│ │ └── index.js
│ │ └── index.html
└── README.md
├── ios-swift
├── start
│ ├── server
│ │ ├── .npmrc
│ │ ├── user_files
│ │ │ └── note.txt
│ │ ├── constants.js
│ │ ├── .env.template
│ │ ├── package.json
│ │ └── user_utils.js
│ ├── client
│ │ ├── SamplePlaidClient
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── AccentColor.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ └── Contents.json
│ │ │ ├── SamplePlaidClient.entitlements
│ │ │ ├── ServerResponses.swift
│ │ │ ├── Info.plist
│ │ │ ├── AppDelegate.swift
│ │ │ ├── PlaidLinkViewController.swift
│ │ │ ├── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ │ ├── InitViewController.swift
│ │ │ ├── SceneDelegate.swift
│ │ │ └── ServerCommunicator.swift
│ │ └── SamplePlaidClient.xcodeproj
│ │ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── FirstTryPlaid.xcscheme
│ └── resources
│ │ └── apple-app-site-association
├── finished
│ ├── server
│ │ ├── .npmrc
│ │ ├── user_files
│ │ │ └── note.txt
│ │ ├── constants.js
│ │ ├── .env.template
│ │ ├── package.json
│ │ └── user_utils.js
│ ├── client
│ │ ├── SamplePlaidClient
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── AccentColor.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ └── Contents.json
│ │ │ ├── SamplePlaidClient.entitlements
│ │ │ ├── ServerResponses.swift
│ │ │ ├── Info.plist
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Base.lproj
│ │ │ │ └── LaunchScreen.storyboard
│ │ │ ├── SceneDelegate.swift
│ │ │ ├── InitViewController.swift
│ │ │ ├── PlaidLinkViewController.swift
│ │ │ └── ServerCommunicator.swift
│ │ └── SamplePlaidClient.xcodeproj
│ │ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── swiftpm
│ │ │ │ └── Package.resolved
│ │ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── FirstTryPlaid.xcscheme
│ └── resources
│ │ └── apple-app-site-association
└── README.md
├── vanilla-js-oauth
├── start
│ ├── .npmrc
│ ├── .env.example
│ ├── package.json
│ ├── public
│ │ ├── connect.html
│ │ ├── index.html
│ │ └── js
│ │ │ ├── index.js
│ │ │ └── connect.js
│ └── server.js
├── finished
│ ├── .npmrc
│ ├── public
│ │ ├── oauth-return.html
│ │ ├── connect.html
│ │ ├── index.html
│ │ └── js
│ │ │ ├── index.js
│ │ │ ├── oauth-return.js
│ │ │ └── connect.js
│ ├── .env.example
│ ├── package.json
│ └── server.js
└── README.md
├── LICENSE
├── README.md
└── .gitignore
/auth/finished/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/auth/start/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/transactions/start/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
--------------------------------------------------------------------------------
/transactions/finished/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
--------------------------------------------------------------------------------
/webhooks/finished/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/webhooks/start/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/ios-swift/start/server/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/ios-swift/finished/server/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/auth/start/user_files/note.txt:
--------------------------------------------------------------------------------
1 | Files to store our user records will go here in lieu of a database.
--------------------------------------------------------------------------------
/auth/finished/user_files/note.txt:
--------------------------------------------------------------------------------
1 | Files to store our user records will go here in lieu of a database.
--------------------------------------------------------------------------------
/ios-swift/finished/server/user_files/note.txt:
--------------------------------------------------------------------------------
1 | Files to store our user records will go here in lieu of a database.
--------------------------------------------------------------------------------
/ios-swift/start/server/user_files/note.txt:
--------------------------------------------------------------------------------
1 | Files to store our user records will go here in lieu of a database.
--------------------------------------------------------------------------------
/auth/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/transactions/start/database/note.txt:
--------------------------------------------------------------------------------
1 | This directory is where our database will live.
2 | (If this directory doesn't exist, the application crashes.)
3 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/transactions/finished/database/note.txt:
--------------------------------------------------------------------------------
1 | This directory is where our database will live.
2 | (If this directory doesn't exist, the application crashes.)
3 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/transactions/start/server/utils.js:
--------------------------------------------------------------------------------
1 | const db = require("./db");
2 |
3 | const getLoggedInUserId = function (req) {
4 | return req.cookies["signedInUser"];
5 | };
6 |
7 | module.exports = { getLoggedInUserId };
8 |
--------------------------------------------------------------------------------
/transactions/finished/server/utils.js:
--------------------------------------------------------------------------------
1 | const db = require("./db");
2 |
3 | const getLoggedInUserId = function (req) {
4 | return req.cookies["signedInUser"];
5 | };
6 |
7 | module.exports = { getLoggedInUserId };
8 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/SamplePlaidClient.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios-swift/start/server/constants.js:
--------------------------------------------------------------------------------
1 | const FIELD_ACCESS_TOKEN = "accessToken";
2 | const FIELD_USER_STATUS = "userStatus";
3 | const FIELD_USER_ID = "userId";
4 | const FIELD_ITEM_ID = "itemId";
5 |
6 | module.exports = {
7 | FIELD_ACCESS_TOKEN,
8 | FIELD_USER_ID,
9 | FIELD_USER_STATUS,
10 | FIELD_ITEM_ID,
11 | };
12 |
--------------------------------------------------------------------------------
/ios-swift/finished/server/constants.js:
--------------------------------------------------------------------------------
1 | const FIELD_ACCESS_TOKEN = "accessToken";
2 | const FIELD_USER_STATUS = "userStatus";
3 | const FIELD_USER_ID = "userId";
4 | const FIELD_ITEM_ID = "itemId";
5 |
6 | module.exports = {
7 | FIELD_ACCESS_TOKEN,
8 | FIELD_USER_ID,
9 | FIELD_USER_STATUS,
10 | FIELD_ITEM_ID,
11 | };
12 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/SamplePlaidClient.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.associated-domains
6 |
7 | applinks:example.com
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios-swift/finished/resources/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "apps": [],
4 | "details": [
5 | {
6 | "appIDs": [
7 | "TEAMID12345.com.example.yourBundleId"
8 | ],
9 | "components": [
10 | {
11 | "/": "/plaid/redirect/*",
12 | "comment": "OAuth Redirect from Plaid"
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ios-swift/start/resources/apple-app-site-association:
--------------------------------------------------------------------------------
1 | {
2 | "applinks": {
3 | "apps": [],
4 | "details": [
5 | {
6 | "appIDs": [
7 | "TEAMID12345.com.example.yourBundleId"
8 | ],
9 | "components": [
10 | {
11 | "/": "/plaid/redirect/*",
12 | "comment": "OAuth Redirect from Plaid"
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "plaid-link-ios",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/plaid/plaid-link-ios.git",
7 | "state" : {
8 | "revision" : "3b5cdf2f4673ee196f51e8132bb8eb6ac694f0a9",
9 | "version" : "4.6.1"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/webhooks/finished/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will determine where Plaid will initially send its webhooks when you
12 | # first set up link
13 | WEBHOOK_URL=https://www.example.com/server/receive_webhook
--------------------------------------------------------------------------------
/webhooks/start/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will determine where Plaid will initially send its webhooks when you
12 | # first set up link
13 | WEBHOOK_URL=https://www.example.com/server/receive_webhook
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/oauth-return.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Oauth Return Page
8 |
9 |
10 | Welcome back!
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/transactions/start/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 |
12 | # (Optional) A URL for the webhook receiver running on port 8001. In the
13 | # tutorial, we'll use ngrok to expose this to the outside world.
14 | WEBHOOK_URL=https://www.example.com/server/receive_webhook
15 |
--------------------------------------------------------------------------------
/transactions/finished/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 |
12 | # (Optional) A URL for the webhook receiver running on port 8001. In the
13 | # tutorial, we'll use ngrok to expose this to the outside world.
14 | WEBHOOK_URL=https://www.example.com/server/receive_webhook
15 |
--------------------------------------------------------------------------------
/transactions/start/server/plaid.js:
--------------------------------------------------------------------------------
1 | const PLAID_ENV = (process.env.PLAID_ENV || "sandbox").toLowerCase();
2 | const { Configuration, PlaidEnvironments, PlaidApi } = require("plaid");
3 |
4 | // Set up the Plaid client library
5 | const plaidConfig = new Configuration({
6 | basePath: PlaidEnvironments[PLAID_ENV],
7 | baseOptions: {
8 | headers: {
9 | "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID,
10 | "PLAID-SECRET": process.env.PLAID_SECRET,
11 | "Plaid-Version": "2020-09-14",
12 | },
13 | },
14 | });
15 |
16 | const plaidClient = new PlaidApi(plaidConfig);
17 |
18 | module.exports = { plaidClient };
19 |
--------------------------------------------------------------------------------
/transactions/finished/server/plaid.js:
--------------------------------------------------------------------------------
1 | const PLAID_ENV = (process.env.PLAID_ENV || "sandbox").toLowerCase();
2 | const { Configuration, PlaidEnvironments, PlaidApi } = require("plaid");
3 |
4 | // Set up the Plaid client library
5 | const plaidConfig = new Configuration({
6 | basePath: PlaidEnvironments[PLAID_ENV],
7 | baseOptions: {
8 | headers: {
9 | "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID,
10 | "PLAID-SECRET": process.env.PLAID_SECRET,
11 | "Plaid-Version": "2020-09-14",
12 | },
13 | },
14 | });
15 |
16 | const plaidClient = new PlaidApi(plaidConfig);
17 |
18 | module.exports = { plaidClient };
19 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/.env.example:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This is used by the express session library to encrypt the session ID. If you
12 | # were to use this feature in production, you would change this to a different
13 | # randomly-generated string.
14 | SESSION_SECRET=YouShouldChangeThisToARandomStringForBetterSecurity
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/.env.example:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This is used by the express session library to encrypt the session ID. If you
12 | # were to use this feature in production, you would change this to a different
13 | # randomly-generated string.
14 | SESSION_SECRET=YouShouldChangeThisToARandomStringForBetterSecurity
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vanilla-js-oauth",
3 | "version": "1.2.0",
4 | "description": "A simple Vanilla JS app that we can use for our OAuth tutorial",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Todd Kerpelman",
12 | "license": "MIT",
13 | "dependencies": {
14 | "body-parser": "^1.19.1",
15 | "dotenv": "^16.0.0",
16 | "express": "^4.18.2",
17 | "express-session": "^1.17.2",
18 | "moment": "^2.29.2",
19 | "plaid": "^30.0.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vanilla-js-oauth",
3 | "version": "1.2.0",
4 | "description": "A simple Vanilla JS app that we can use for our OAuth tutorial",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Todd Kerpelman",
12 | "license": "MIT",
13 | "dependencies": {
14 | "body-parser": "^1.20.1",
15 | "dotenv": "^16.0.3",
16 | "express": "^4.17.2",
17 | "express-session": "^1.17.2",
18 | "moment": "^2.29.4",
19 | "plaid": "^30.0.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios-swift/start/server/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will represent the number of our logged in user. Useful when you want
12 | # to switch back and forth between users in development.
13 | # Changing this value while running `npm run watch` will restart the server so
14 | # you can simulate logging in as new user. (Just refresh your client, too!)
15 | USER_ID=
16 |
--------------------------------------------------------------------------------
/ios-swift/finished/server/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will represent the number of our logged in user. Useful when you want
12 | # to switch back and forth between users in development.
13 | # Changing this value while running `npm run watch` will restart the server so
14 | # you can simulate logging in as new user. (Just refresh your client, too!)
15 | USER_ID=
16 |
--------------------------------------------------------------------------------
/transactions/start/server/routes/banks.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const db = require("../db");
3 | const { getLoggedInUserId } = require("../utils");
4 | const { plaidClient } = require("../plaid");
5 |
6 | const router = express.Router();
7 |
8 | router.get("/list", async (req, res, next) => {
9 | try {
10 | const userId = getLoggedInUserId(req);
11 | const result = await db.getBankNamesForUser(userId);
12 | res.json(result);
13 | } catch (error) {
14 | next(error);
15 | }
16 | });
17 |
18 | router.post("/deactivate", async (req, res, next) => {
19 | try {
20 | res.json({ todo: "Implement this method" });
21 | } catch (error) {
22 | next(error);
23 | }
24 | });
25 |
26 | module.exports = router;
27 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/public/connect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Connect!
9 |
10 |
11 | Connect my bank
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/connect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Connect!
9 |
10 |
11 | Connect my bank
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ios-swift/start/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ios-server",
3 | "version": "1.0.0",
4 | "description": "Basic server to use for our iOS sample app",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "nodemonConfig": {
12 | "ignore": [
13 | "user_files/*"
14 | ],
15 | "watch": [
16 | "*.js",
17 | ".env"
18 | ]
19 | },
20 | "author": "Todd Kerpelman",
21 | "license": "MIT",
22 | "dependencies": {
23 | "body-parser": "^1.20.2",
24 | "dotenv": "^16.3.1",
25 | "express": "^4.18.2",
26 | "moment": "^2.29.4",
27 | "nodemon": "^3.0.1",
28 | "plaid": "^30.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ios-swift/finished/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ios-server",
3 | "version": "1.0.0",
4 | "description": "Basic server to use for our iOS sample app",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "nodemonConfig": {
12 | "ignore": [
13 | "user_files/*"
14 | ],
15 | "watch": [
16 | "*.js",
17 | ".env"
18 | ]
19 | },
20 | "author": "Todd Kerpelman",
21 | "license": "MIT",
22 | "dependencies": {
23 | "body-parser": "^1.20.2",
24 | "dotenv": "^16.3.1",
25 | "express": "^4.18.2",
26 | "moment": "^2.29.4",
27 | "nodemon": "^3.0.1",
28 | "plaid": "^30.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/auth/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth-tutorial",
3 | "version": "1.0.0",
4 | "description": "Auth tutorial to be added to the plaid-tutorial-resources repo",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "nodemonConfig": {
12 | "ignore": [
13 | "user_files/*"
14 | ],
15 | "watch": [
16 | "server.js",
17 | ".env"
18 | ]
19 | },
20 | "author": "Todd Kerpelman",
21 | "license": "MIT",
22 | "dependencies": {
23 | "body-parser": "^1.20.0",
24 | "dotenv": "^16.0.1",
25 | "express": "^4.18.1",
26 | "moment": "^2.29.4",
27 | "nodemon": "^3.1.7",
28 | "plaid": "^30.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/auth/finished/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth-tutorial",
3 | "version": "1.0.0",
4 | "description": "Auth tutorial to be added to the plaid-tutorial-resources repo",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "nodemonConfig": {
12 | "ignore": [
13 | "user_files/*"
14 | ],
15 | "watch": [
16 | "server.js",
17 | ".env"
18 | ]
19 | },
20 | "author": "Todd Kerpelman",
21 | "license": "MIT",
22 | "dependencies": {
23 | "body-parser": "^1.20.0",
24 | "dotenv": "^16.0.1",
25 | "express": "^4.18.1",
26 | "moment": "^2.29.4",
27 | "nodemon": "^3.1.7",
28 | "plaid": "^30.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/ServerResponses.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserStatus.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Dave Troupe on 8/30/23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum UserConnectionStatus: String, Codable {
11 | case connected
12 | case disconnected
13 | }
14 |
15 | struct UserStatusResponse: Codable {
16 | let userStatus: UserConnectionStatus
17 | let userId: String
18 | }
19 |
20 | struct LinkTokenCreateResponse: Codable {
21 | let linkToken: String
22 | let expiration: String
23 | }
24 |
25 | struct SwapPublicTokenResponse: Codable {
26 | let success: Bool
27 | }
28 |
29 | struct SimpleAuthResponse: Codable{
30 | let accountName: String
31 | let accountMask: String
32 | let routingNumber: String
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/ServerResponses.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserStatus.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Dave Troupe on 8/30/23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum UserConnectionStatus: String, Codable {
11 | case connected
12 | case disconnected
13 | }
14 |
15 | struct UserStatusResponse: Codable {
16 | let userStatus: UserConnectionStatus
17 | let userId: String
18 | }
19 |
20 | struct LinkTokenCreateResponse: Codable {
21 | let linkToken: String
22 | let expiration: String
23 | }
24 |
25 | struct SwapPublicTokenResponse: Codable {
26 | let success: Bool
27 | }
28 |
29 | struct SimpleAuthResponse: Codable{
30 | let accountName: String
31 | let accountMask: String
32 | let routingNumber: String
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/webhooks/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webhook-test",
3 | "version": "1.0.0",
4 | "description": "A simple Vanilla JS app that we can use for our webhook tutorial",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js --watch server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Todd Kerpelman",
12 | "license": "MIT",
13 | "dependencies": {
14 | "body-parser": "^1.20.0",
15 | "dotenv": "^16.0.0",
16 | "express": "^4.18.1",
17 | "jose": "^4.8.1",
18 | "js-sha256": "^0.9.0",
19 | "jsonwebtoken": "^9.0.2",
20 | "jwt-decode": "^3.1.2",
21 | "moment": "^2.29.3",
22 | "nodemon": "^3.1.7",
23 | "plaid": "^30.0.0",
24 | "secure-compare": "^3.0.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/webhooks/finished/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webhook-test",
3 | "version": "1.0.0",
4 | "description": "A simple Vanilla JS app that we can use for our webhook tutorial",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "watch": "nodemon server.js --watch server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Todd Kerpelman",
12 | "license": "MIT",
13 | "dependencies": {
14 | "body-parser": "^1.20.0",
15 | "dotenv": "^16.0.0",
16 | "express": "^4.18.1",
17 | "jose": "^4.8.1",
18 | "js-sha256": "^0.9.0",
19 | "jsonwebtoken": "^9.0.2",
20 | "jwt-decode": "^3.1.2",
21 | "moment": "^2.29.3",
22 | "nodemon": "^3.1.7",
23 | "plaid": "^30.0.0",
24 | "secure-compare": "^3.0.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/transactions/finished/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transactions-tutorial",
3 | "version": "1.0.0",
4 | "description": "Transactions tutorial for the plaid-tutorial-resources repo",
5 | "main": "server/server.js",
6 | "scripts": {
7 | "start": "node server/server.js",
8 | "watch": "nodemon server/server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "css": "sass scss/custom.scss:public/css/custom_bootstrap.css"
11 | },
12 | "author": "Todd Kerpelman",
13 | "license": "MIT",
14 | "dependencies": {
15 | "body-parser": "^1.20.1",
16 | "bootstrap": "^5.2.3",
17 | "cookie-parser": "^1.4.6",
18 | "dotenv": "^16.0.3",
19 | "escape-html": "^1.0.3",
20 | "express": "^4.18.2",
21 | "moment": "^2.29.4",
22 | "nodemon": "^3.1.7",
23 | "plaid": "^30.0.0",
24 | "sqlite": "^4.1.2",
25 | "sqlite3": "^5.1.6",
26 | "uuid": "^9.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/transactions/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transactions-tutorial",
3 | "version": "1.0.0",
4 | "description": "Transactions tutorial for the plaid-tutorial-resources repo",
5 | "main": "server/server.js",
6 | "scripts": {
7 | "start": "node server/server.js",
8 | "watch": "nodemon server/server.js",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "css": "sass scss/custom.scss:public/css/custom_bootstrap.css"
11 | },
12 | "author": "Todd Kerpelman",
13 | "license": "MIT",
14 | "dependencies": {
15 | "body-parser": "^1.20.1",
16 | "bootstrap": "^5.2.3",
17 | "cookie-parser": "^1.4.6",
18 | "dotenv": "^16.0.3",
19 | "escape-html": "^1.0.3",
20 | "express": "^4.18.2",
21 | "moment": "^2.29.4",
22 | "nodemon": "^3.1.7",
23 | "plaid": "^30.0.0",
24 | "sqlite": "^4.1.2",
25 | "sqlite3": "^5.1.6",
26 | "uuid": "^9.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/transactions/finished/server/routes/banks.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const db = require("../db");
3 | const { getLoggedInUserId } = require("../utils");
4 | const { plaidClient } = require("../plaid");
5 |
6 | const router = express.Router();
7 |
8 | router.get("/list", async (req, res, next) => {
9 | try {
10 | const userId = getLoggedInUserId(req);
11 | const result = await db.getBankNamesForUser(userId);
12 | res.json(result);
13 | } catch (error) {
14 | next(error);
15 | }
16 | });
17 |
18 | router.post("/deactivate", async (req, res, next) => {
19 | try {
20 | const itemId = req.body.itemId;
21 | const userId = getLoggedInUserId(req);
22 | const { access_token: accessToken } = await db.getItemInfoForUser(
23 | itemId,
24 | userId
25 | );
26 | await plaidClient.itemRemove({
27 | access_token: accessToken,
28 | });
29 | await db.deactivateItem(itemId);
30 |
31 | res.json({ removed: itemId });
32 | } catch (error) {
33 | next(error);
34 | }
35 | });
36 |
37 | module.exports = router;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 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 |
--------------------------------------------------------------------------------
/transactions/start/server/routes/debug.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { getLoggedInUserId } = require("../utils");
3 | const db = require("../db");
4 | const { plaidClient } = require("../plaid");
5 | const {
6 | SandboxItemFireWebhookRequestWebhookCodeEnum,
7 | WebhookType,
8 | } = require("plaid");
9 |
10 | const router = express.Router();
11 |
12 | /**
13 | * Sometimes you wanna run some custom server code. This seemed like the
14 | * easiest way to do it. Don't do this in a real application.
15 | */
16 | router.post("/run", async (req, res, next) => {
17 | try {
18 | const userId = getLoggedInUserId(req);
19 | res.json({ status: "done" });
20 | } catch (error) {
21 | next(error);
22 | }
23 | });
24 |
25 | /**
26 | * This code will eventually be used to generate a test webhook, which can
27 | * be useful in sandbox mode where webhooks aren't quite generated like
28 | * they are in production.
29 | */
30 | router.post("/generate_webhook", async (req, res, next) => {
31 | try {
32 | res.json({ todo: "Implement this method" });
33 | } catch (error) {
34 | next(error);
35 | }
36 | });
37 |
38 | module.exports = router;
39 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Welcome!
9 |
10 |
11 | Welcome!
12 |
17 |
18 |
You're connected! But if you want to
19 | create another connection,
20 |
go here .
21 |
22 |
Get transactions!
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Welcome!
9 |
10 |
11 | Welcome!
12 |
17 |
18 |
You're connected! But if you want to
19 | create another connection,
20 |
go here .
21 |
22 |
Get transactions!
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/transactions/start/server/simpleTransactionObject.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A simple object to pass to our database functions that represents the data
3 | * our application cares about from the Plaid transaction endpoint
4 | */
5 | class SimpleTransaction {
6 | constructor(
7 | id,
8 | userId,
9 | accountId,
10 | category,
11 | date,
12 | authorizedDate,
13 | name,
14 | amount,
15 | currencyCode,
16 | pendingTransactionId
17 | ) {
18 | this.id = id;
19 | this.userId = userId;
20 | this.accountId = accountId;
21 | this.category = category;
22 | this.date = date;
23 | this.authorizedDate = authorizedDate;
24 | this.name = name;
25 | this.amount = amount;
26 | this.currencyCode = currencyCode;
27 | this.pendingTransactionId = pendingTransactionId;
28 | }
29 |
30 | /**
31 | * Static factory method for creating the SimpleTransaction object
32 | *
33 | * @param {import("plaid").Transaction} txnObj The transaction object returned from the Plaid API
34 | * @param {string} userId The userID
35 | * @returns SimpleTransaction
36 | */
37 | static fromPlaidTransaction(txnObj, userId) {
38 | // TODO: Fill this out
39 | }
40 | }
41 |
42 | module.exports = { SimpleTransaction };
43 |
--------------------------------------------------------------------------------
/auth/finished/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will represent the number of our logged in user. Useful when you want
12 | # to switch back and forth between users in development.
13 | # Changing this value while running `npm run watch` will restart the server so
14 | # you can simulate logging in as new user. (Just refresh your client, too!)
15 | USER_ID=1
16 |
17 | # This will determine where Plaid will initially send its webhooks when you
18 | # first set up link. For the "extra credit" to work, you will also need to
19 | # add this URL to https://dashboard.plaid.com/developers/webhooks under the
20 | # "Bank transfer" event type.
21 | WEBHOOK_URL=https://123-456-789.ngrok.io/server/receive_webhook
22 |
23 | # This app was designed to be used with a single account per Item. For best
24 | # results, create a Link customization that only allows a single account
25 | # (https://dashboard.plaid.com/link/account-select) and add the name of that
26 | # customization here
27 | LINK_CUSTOM_NAME=single_account
--------------------------------------------------------------------------------
/auth/start/.env.template:
--------------------------------------------------------------------------------
1 | # Copy this over to .env before you fill it out!
2 |
3 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys
4 | PLAID_CLIENT_ID=
5 | PLAID_SECRET=
6 |
7 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment
8 | # Use 'production' to run with real data
9 | PLAID_ENV=sandbox
10 |
11 | # This will represent the number of our logged in user. Useful when you want
12 | # to switch back and forth between users in development.
13 | # Changing this value while running `npm run watch` will restart the server so
14 | # you can simulate logging in as new user. (Just refresh your client, too!)
15 | USER_ID=1
16 |
17 | # This will determine where Plaid will initially send its webhooks when you
18 | # first set up link. For the "extra credit" to work, you will also need to
19 | # add this URL to https://dashboard.plaid.com/developers/webhooks under the
20 | # "Bank transfer" event type.
21 | WEBHOOK_URL=https://123-456-789.ngrok.io/server/receive_webhook
22 |
23 | # This app was designed to be used with a single account per Item. For best
24 | # results, create a Link customization that only allows a single account
25 | # (https://dashboard.plaid.com/link/account-select) and add the name of that
26 | # customization here
27 | LINK_CUSTOM_NAME=single_account
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | // MARK: UISceneSession Lifecycle
19 |
20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
21 | // Called when a new scene session is being created.
22 | // Use this method to select a configuration to create the new scene with.
23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
24 | }
25 |
26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
27 | // Called when the user discards a scene session.
28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
30 | }
31 |
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | // MARK: UISceneSession Lifecycle
19 |
20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
21 | // Called when a new scene session is being created.
22 | // Use this method to select a configuration to create the new scene with.
23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
24 | }
25 |
26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
27 | // Called when the user discards a scene session.
28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
30 | }
31 |
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/transactions/start/testData.js:
--------------------------------------------------------------------------------
1 | // Want to test out modified transactions? Copy this bit of code to the end
2 | // of your fetchNewSyncData call
3 |
4 | allData.modified.push({
5 | account_id: "USE_AN_EXISTING_ACCOUNT_ID",
6 | account_owner: null,
7 | amount: 6.33,
8 | authorized_date: "2021-03-23",
9 | authorized_datetime: null,
10 | category: ["Travel", "Taxi"],
11 | category_id: "22016000",
12 | check_number: null,
13 | date: "2021-03-24",
14 | datetime: null,
15 | iso_currency_code: "USD",
16 | location: {
17 | address: null,
18 | city: null,
19 | country: null,
20 | lat: null,
21 | lon: null,
22 | postal_code: null,
23 | region: null,
24 | store_number: null,
25 | },
26 | merchant_name: "Uber",
27 | name: "Uber 072515 SF**POOL**",
28 | payment_channel: "online",
29 | payment_meta: {
30 | by_order_of: null,
31 | payee: null,
32 | payer: null,
33 | payment_method: null,
34 | payment_processor: null,
35 | ppd_id: null,
36 | reason: null,
37 | reference_number: null,
38 | },
39 | pending: false,
40 | pending_transaction_id: null,
41 | personal_finance_category: {
42 | detailed: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES",
43 | primary: "TRANSPORTATION",
44 | },
45 | transaction_code: null,
46 | transaction_id: "USE_AN_EXISTING_TRANSACTION_ID",
47 | transaction_type: "special",
48 | unofficial_currency_code: null,
49 | });
50 |
51 | // And here's some code you can use to test removed
52 | // transactions
53 |
54 | allData.removed.push({
55 | transaction_id: "USE_AN_EXISTING_TRANSACTION_ID",
56 | });
57 |
--------------------------------------------------------------------------------
/transactions/finished/testData.js:
--------------------------------------------------------------------------------
1 | // Want to test out modified transactions? Copy this bit of code to the end
2 | // of your fetchNewSyncData call
3 |
4 | allData.modified.push({
5 | account_id: "USE_AN_EXISTING_ACCOUNT_ID",
6 | account_owner: null,
7 | amount: 6.33,
8 | authorized_date: "2021-03-23",
9 | authorized_datetime: null,
10 | category: ["Travel", "Taxi"],
11 | category_id: "22016000",
12 | check_number: null,
13 | date: "2021-03-24",
14 | datetime: null,
15 | iso_currency_code: "USD",
16 | location: {
17 | address: null,
18 | city: null,
19 | country: null,
20 | lat: null,
21 | lon: null,
22 | postal_code: null,
23 | region: null,
24 | store_number: null,
25 | },
26 | merchant_name: "Uber",
27 | name: "Uber 072515 SF**POOL**",
28 | payment_channel: "online",
29 | payment_meta: {
30 | by_order_of: null,
31 | payee: null,
32 | payer: null,
33 | payment_method: null,
34 | payment_processor: null,
35 | ppd_id: null,
36 | reason: null,
37 | reference_number: null,
38 | },
39 | pending: false,
40 | pending_transaction_id: null,
41 | personal_finance_category: {
42 | detailed: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES",
43 | primary: "TRANSPORTATION",
44 | },
45 | transaction_code: null,
46 | transaction_id: "USE_AN_EXISTING_TRANSACTION_ID",
47 | transaction_type: "special",
48 | unofficial_currency_code: null,
49 | });
50 |
51 | // And here's some code you can use to test removed
52 | // transactions
53 |
54 | allData.removed.push({
55 | transaction_id: "USE_AN_EXISTING_TRANSACTION_ID",
56 | });
57 |
--------------------------------------------------------------------------------
/transactions/finished/server/simpleTransactionObject.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A simple object to pass to our database functions that represents the data
3 | * our application cares about from the Plaid transaction endpoint
4 | */
5 | class SimpleTransaction {
6 | constructor(
7 | id,
8 | userId,
9 | accountId,
10 | category,
11 | date,
12 | authorizedDate,
13 | name,
14 | amount,
15 | currencyCode,
16 | pendingTransactionId
17 | ) {
18 | this.id = id;
19 | this.userId = userId;
20 | this.accountId = accountId;
21 | this.category = category;
22 | this.date = date;
23 | this.authorizedDate = authorizedDate;
24 | this.name = name;
25 | this.amount = amount;
26 | this.currencyCode = currencyCode;
27 | this.pendingTransactionId = pendingTransactionId;
28 | }
29 |
30 | /**
31 | * Static factory method for creating the SimpleTransaction object
32 | *
33 | * @param {import("plaid").Transaction} txnObj The transaction object returned from the Plaid API
34 | * @param {string} userId The userID
35 | * @returns SimpleTransaction
36 | */
37 | static fromPlaidTransaction(txnObj, userId) {
38 | return new SimpleTransaction(
39 | txnObj.transaction_id,
40 | userId,
41 | txnObj.account_id,
42 | txnObj.personal_finance_category.primary,
43 | txnObj.date,
44 | txnObj.authorized_date,
45 | txnObj.merchant_name ?? txnObj.name,
46 | txnObj.amount,
47 | txnObj.iso_currency_code,
48 | txnObj.pending_transaction_id
49 | );
50 | }
51 | }
52 |
53 | module.exports = { SimpleTransaction };
54 |
--------------------------------------------------------------------------------
/transactions/finished/server/routes/debug.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { getLoggedInUserId } = require("../utils");
3 | const db = require("../db");
4 | const { plaidClient } = require("../plaid");
5 | const {
6 | SandboxItemFireWebhookRequestWebhookCodeEnum,
7 | WebhookType,
8 | } = require("plaid");
9 |
10 | const router = express.Router();
11 |
12 | /**
13 | * Sometimes you wanna run some custom server code. This seemed like the
14 | * easiest way to do it. Don't do this in a real application.
15 | */
16 | router.post("/run", async (req, res, next) => {
17 | try {
18 | const userId = getLoggedInUserId(req);
19 | res.json({ status: "done" });
20 | } catch (error) {
21 | next(error);
22 | }
23 | });
24 |
25 | /**
26 | * This code will eventually be used to generate a test webhook, which can
27 | * be useful in sandbox mode where webhooks aren't quite generated like
28 | * they are in production.
29 | */
30 | router.post("/generate_webhook", async (req, res, next) => {
31 | try {
32 | const userId = getLoggedInUserId(req);
33 | const itemsAndTokens = await db.getItemsAndAccessTokensForUser(userId);
34 | const randomItem =
35 | itemsAndTokens[Math.floor(Math.random() * itemsAndTokens.length)];
36 | const accessToken = randomItem.access_token;
37 | const result = await plaidClient.sandboxItemFireWebhook({
38 | webhook_code:
39 | SandboxItemFireWebhookRequestWebhookCodeEnum.SyncUpdatesAvailable,
40 | access_token: accessToken,
41 | });
42 | res.json(result.data);
43 | } catch (error) {
44 | next(error);
45 | }
46 | });
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/PlaidLinkViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaidLinkViewController.swift
3 | // SamplePlaidClients
4 | //
5 | // Created by Todd Kerpelman on 8/18/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class PlaidLinkViewController: UIViewController {
11 | @IBOutlet var startLinkButton: UIButton!
12 | let communicator = ServerCommunicator()
13 | var linkToken: String?
14 | // var handler: Handler?
15 |
16 |
17 | private func createLinkConfiguration(linkToken: String) -> Any? {
18 | // Create our link configuration object
19 | // This return type will be a LinkTokenConfiguration object
20 | return nil
21 | }
22 |
23 | @IBAction func startLinkWasPressed(_ sender: Any) {
24 | // Handle the button being clicked
25 |
26 | }
27 |
28 | private func exchangePublicTokenForAccessToken(_ publicToken: String) {
29 | // Exchange our public token for an access token
30 | }
31 |
32 |
33 | private func fetchLinkToken() {
34 | // Fetch a link token from our server
35 |
36 | }
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 | self.startLinkButton.isEnabled = false
41 | fetchLinkToken()
42 | }
43 |
44 |
45 | /*
46 | // MARK: - Navigation
47 |
48 | // In a storyboard-based application, you will often want to do a little preparation before navigation
49 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
50 | // Get the new view controller using segue.destination.
51 | // Pass the selected object to the new view controller.
52 | }
53 | */
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/auth/start/public/js/connect.js:
--------------------------------------------------------------------------------
1 | import { checkConnectedStatus } from "./client.js";
2 | import { callMyServer } from "./utilities.js";
3 |
4 | let linkTokenData;
5 |
6 | export const initializeLink = async function () {
7 | linkTokenData = await callMyServer("/server/generate_link_token", true);
8 | document.querySelector("#startLink").classList.remove("opacity-50");
9 | };
10 |
11 | export const startMicroVerification = async function () {
12 | // TODO: Implement this
13 | };
14 |
15 | const startLink = function () {
16 | if (linkTokenData === undefined) {
17 | return;
18 | }
19 | const handler = Plaid.create({
20 | token: linkTokenData.link_token,
21 | onSuccess: async (publicToken, metadata) => {
22 | console.log(`ONSUCCESS: Metadata ${JSON.stringify(metadata)}`);
23 | console.log(
24 | `I have a public token: ${publicToken} I should exchange this`
25 | );
26 | await exchangeToken(publicToken);
27 | },
28 | onExit: (err, metadata) => {
29 | console.log(
30 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
31 | metadata
32 | )}`
33 | );
34 | },
35 | onEvent: (eventName, metadata) => {
36 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`);
37 | },
38 | });
39 | handler.open();
40 | };
41 |
42 | async function exchangeToken(publicToken) {
43 | await callMyServer("/server/swap_public_token", true, {
44 | public_token: publicToken,
45 | });
46 | console.log("Done exchanging our token. I'll re-fetch our status");
47 | await checkConnectedStatus();
48 | }
49 |
50 | document.querySelector("#startLink").addEventListener("click", () => {
51 | startLink(false);
52 | });
53 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/js/index.js:
--------------------------------------------------------------------------------
1 | const checkConnectedStatus = async function () {
2 | try {
3 | const connectedResponse = await fetch(`/api/is_user_connected`);
4 | const connectedData = await connectedResponse.json();
5 | console.log(JSON.stringify(connectedData));
6 | if (connectedData.status === true) {
7 | document.querySelector("#connectedUI").classList.remove("hidden");
8 | showInstitutionName();
9 | } else {
10 | document.querySelector("#disconnectedUI").classList.remove("hidden");
11 | }
12 | } catch (error) {
13 | console.error(`We encountered an error: ${error}`);
14 | }
15 | };
16 |
17 | const showInstitutionName = async function () {
18 | const bankData = await fetch("/api/get_bank_name");
19 | const bankJSON = await bankData.json();
20 | console.log(JSON.stringify(bankJSON));
21 | document.querySelector(
22 | "#connectDetails"
23 | ).textContent = `You are connected to ${bankJSON.name ?? "Unknown"}! `;
24 | };
25 |
26 | // Grab a list of most recent transactions
27 | const getTransactions = async function () {
28 | const transactionResponse = await fetch(`/api/transactions`);
29 | const transactionData = await transactionResponse.json();
30 | const simplifiedData = transactionData.transactions.map((item) => {
31 | return {
32 | date: item.date,
33 | name: item.name,
34 | amount: `$${item.amount.toFixed(2)}`,
35 | categories: item.category.join(", "),
36 | };
37 | });
38 | console.table(simplifiedData);
39 | document.querySelector("#output").textContent =
40 | JSON.stringify(simplifiedData);
41 | };
42 |
43 | document
44 | .querySelector("#getTransactions")
45 | .addEventListener("click", getTransactions);
46 |
47 | checkConnectedStatus();
48 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/public/js/index.js:
--------------------------------------------------------------------------------
1 | const checkConnectedStatus = async function () {
2 | try {
3 | const connectedResponse = await fetch(`/api/is_user_connected`);
4 | const connectedData = await connectedResponse.json();
5 | console.log(JSON.stringify(connectedData));
6 | if (connectedData.status === true) {
7 | document.querySelector("#connectedUI").classList.remove("hidden");
8 | showInstitutionName();
9 | } else {
10 | document.querySelector("#disconnectedUI").classList.remove("hidden");
11 | }
12 | } catch (error) {
13 | console.error(`We encountered an error: ${error}`);
14 | }
15 | };
16 |
17 | const showInstitutionName = async function () {
18 | const bankData = await fetch("/api/get_bank_name");
19 | const bankJSON = await bankData.json();
20 | console.log(JSON.stringify(bankJSON));
21 | document.querySelector(
22 | "#connectDetails"
23 | ).textContent = `You are connected to ${bankJSON.name ?? "Unknown"}! `;
24 | };
25 |
26 | // Grab a list of most recent transactions
27 | const getTransactions = async function () {
28 | const transactionResponse = await fetch(`/api/transactions`);
29 | const transactionData = await transactionResponse.json();
30 | const simplifiedData = transactionData.transactions.map((item) => {
31 | return {
32 | date: item.date,
33 | name: item.name,
34 | amount: `$${item.amount.toFixed(2)}`,
35 | categories: item.category.join(", "),
36 | };
37 | });
38 | console.table(simplifiedData);
39 | document.querySelector("#output").textContent =
40 | JSON.stringify(simplifiedData);
41 | };
42 |
43 | document
44 | .querySelector("#getTransactions")
45 | .addEventListener("click", getTransactions);
46 |
47 | checkConnectedStatus();
48 |
--------------------------------------------------------------------------------
/transactions/start/public/js/link.js:
--------------------------------------------------------------------------------
1 | import { callMyServer } from "./utils.js";
2 |
3 | /**
4 | * Start Link and define the callbacks we will call if a user completes the
5 | * flow or exits early
6 | */
7 | export const startLink = async function (customSuccessHandler) {
8 | const linkTokenData = await fetchLinkToken();
9 | if (linkTokenData === undefined) {
10 | return;
11 | }
12 | const handler = Plaid.create({
13 | token: linkTokenData.link_token,
14 | onSuccess: async (publicToken, metadata) => {
15 | console.log(`Finished with Link! ${JSON.stringify(metadata)}`);
16 | await exchangePublicToken(publicToken);
17 | customSuccessHandler();
18 | },
19 | onExit: async (err, metadata) => {
20 | console.log(
21 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
22 | metadata
23 | )}`
24 | );
25 | },
26 | onEvent: (eventName, metadata) => {
27 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`);
28 | },
29 | });
30 | handler.open();
31 | };
32 |
33 | /**
34 | * To start Link, we need to fetch a Link token from the user. We'll save this
35 | * as our `linkTokenData` variable defined at the beginning of our file.
36 | */
37 | const fetchLinkToken = async function () {
38 | const linkTokenData = await callMyServer(
39 | "/server/tokens/generate_link_token",
40 | true
41 | );
42 | return linkTokenData;
43 | };
44 |
45 | /**
46 | * Exchange our Link token data for an access token
47 | */
48 | const exchangePublicToken = async (publicToken) => {
49 | await callMyServer("/server/tokens/exchange_public_token", true, {
50 | publicToken: publicToken,
51 | });
52 | console.log("Done exchanging our token.");
53 | };
54 |
--------------------------------------------------------------------------------
/transactions/finished/public/js/link.js:
--------------------------------------------------------------------------------
1 | import { callMyServer } from "./utils.js";
2 |
3 | /**
4 | * Start Link and define the callbacks we will call if a user completes the
5 | * flow or exits early
6 | */
7 | export const startLink = async function (customSuccessHandler) {
8 | const linkTokenData = await fetchLinkToken();
9 | if (linkTokenData === undefined) {
10 | return;
11 | }
12 | const handler = Plaid.create({
13 | token: linkTokenData.link_token,
14 | onSuccess: async (publicToken, metadata) => {
15 | console.log(`Finished with Link! ${JSON.stringify(metadata)}`);
16 | await exchangePublicToken(publicToken);
17 | customSuccessHandler();
18 | },
19 | onExit: async (err, metadata) => {
20 | console.log(
21 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
22 | metadata
23 | )}`
24 | );
25 | },
26 | onEvent: (eventName, metadata) => {
27 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`);
28 | },
29 | });
30 | handler.open();
31 | };
32 |
33 | /**
34 | * To start Link, we need to fetch a Link token from the user. We'll save this
35 | * as our `linkTokenData` variable defined at the beginning of our file.
36 | */
37 | const fetchLinkToken = async function () {
38 | const linkTokenData = await callMyServer(
39 | "/server/tokens/generate_link_token",
40 | true
41 | );
42 | return linkTokenData;
43 | };
44 |
45 | /**
46 | * Exchange our Link token data for an access token
47 | */
48 | const exchangePublicToken = async (publicToken) => {
49 | await callMyServer("/server/tokens/exchange_public_token", true, {
50 | publicToken: publicToken,
51 | });
52 | console.log("Done exchanging our token.");
53 | };
54 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/public/js/connect.js:
--------------------------------------------------------------------------------
1 | let linkTokenData;
2 |
3 | const initializeLink = async function () {
4 | const linkTokenResponse = await fetch(`/api/create_link_token`);
5 | linkTokenData = await linkTokenResponse.json();
6 | document.querySelector("#startLink").classList.remove("opacity-50");
7 | console.log(JSON.stringify(linkTokenData));
8 | };
9 |
10 | const startLink = function () {
11 | if (linkTokenData === undefined) {
12 | return;
13 | }
14 | const handler = Plaid.create({
15 | token: linkTokenData.link_token,
16 | onSuccess: async (publicToken, metadata) => {
17 | console.log(
18 | `I have a public token: ${publicToken} I should exchange this`
19 | );
20 | await exchangeToken(publicToken);
21 | },
22 | onExit: (err, metadata) => {
23 | console.log(
24 | `I'm all done. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
25 | metadata
26 | )}`
27 | );
28 | },
29 | onEvent: (eventName, metadata) => {
30 | console.log(`Event ${eventName}`);
31 | },
32 | });
33 | handler.open();
34 | };
35 |
36 | async function exchangeToken(publicToken) {
37 | const tokenExchangeResponse = await fetch(`/api/exchange_public_token`, {
38 | method: "POST",
39 | headers: { "Content-type": "application/json" },
40 | body: JSON.stringify({ public_token: publicToken }),
41 | });
42 | // This is where I'd add our error checking... if our server returned any
43 | // errors.
44 | const tokenExchangeData = await tokenExchangeResponse.json();
45 | console.log("Done exchanging our token");
46 | window.location.href = "index.html";
47 | }
48 |
49 | document.querySelector("#startLink").addEventListener("click", startLink);
50 |
51 | initializeLink();
52 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/js/oauth-return.js:
--------------------------------------------------------------------------------
1 | function finishOAuth() {
2 | const storedTokenData = localStorage.getItem("linkTokenData");
3 | console.log(`I retrieved ${storedTokenData} from local storage`);
4 | const linkTokenData = JSON.parse(storedTokenData);
5 |
6 | const handler = Plaid.create({
7 | token: linkTokenData.link_token,
8 | receivedRedirectUri: window.location.href,
9 | onSuccess: async (publicToken, metadata) => {
10 | console.log(
11 | `I have a public token: ${publicToken} I should exchange this`
12 | );
13 | await exchangeToken(publicToken);
14 | },
15 | onExit: (err, metadata) => {
16 | console.log(
17 | `I'm all done. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
18 | metadata
19 | )}`
20 | );
21 | if (err !== null) {
22 | document.querySelector("#userMessage").innerHTML =
23 | "Oh no! We got some kind of error! Please try again. ";
24 | }
25 | },
26 | onEvent: (eventName, metadata) => {
27 | console.log(`Event ${eventName}`);
28 | },
29 | });
30 | handler.open();
31 | }
32 |
33 | // TODO: Remove this duplicate code by putting it into a module or something
34 | async function exchangeToken(publicToken) {
35 | const tokenExchangeResponse = await fetch(`/api/exchange_public_token`, {
36 | method: "POST",
37 | headers: { "Content-type": "application/json" },
38 | body: JSON.stringify({ public_token: publicToken }),
39 | });
40 | // This is where I'd add our error checking... if our server returned any
41 | // errors.
42 | const tokenExchangeData = await tokenExchangeResponse.json();
43 | console.log("Done exchanging our token");
44 | window.location.href = "index.html";
45 | }
46 |
47 | finishOAuth();
48 |
--------------------------------------------------------------------------------
/webhooks/start/public/js/connect.js:
--------------------------------------------------------------------------------
1 | import { checkConnectedStatus } from "./index.js";
2 |
3 | let linkTokenData;
4 |
5 | export const initializeLink = async function () {
6 | const linkTokenResponse = await fetch("/server/generate_link_token", {
7 | method: "POST",
8 | });
9 | linkTokenData = await linkTokenResponse.json();
10 | document.querySelector("#startLink").classList.remove("opacity-50");
11 | console.log(JSON.stringify(linkTokenData));
12 | };
13 |
14 | const startLink = function () {
15 | if (linkTokenData === undefined) {
16 | return;
17 | }
18 | const handler = Plaid.create({
19 | token: linkTokenData.link_token,
20 | onSuccess: async (publicToken, metadata) => {
21 | console.log(
22 | `I have a public token: ${publicToken} I should exchange this`
23 | );
24 | await exchangeToken(publicToken);
25 | },
26 | onExit: (err, metadata) => {
27 | console.log(
28 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
29 | metadata
30 | )}`
31 | );
32 | },
33 | onEvent: (eventName, metadata) => {
34 | console.log(`Event ${eventName}`);
35 | },
36 | });
37 | handler.open();
38 | };
39 |
40 | async function exchangeToken(publicToken) {
41 | const tokenExchangeResponse = await fetch("/server/swap_public_token", {
42 | method: "POST",
43 | headers: { "Content-type": "application/json" },
44 | body: JSON.stringify({ public_token: publicToken }),
45 | });
46 | // This is where I'd add our error checking... if our server returned any
47 | // errors.
48 | const tokenExchangeData = await tokenExchangeResponse.json();
49 | console.log("Done exchanging our token");
50 | await checkConnectedStatus();
51 | }
52 |
53 | document.querySelector("#startLink").addEventListener("click", startLink);
54 |
--------------------------------------------------------------------------------
/auth/finished/public/js/connect.js:
--------------------------------------------------------------------------------
1 | import { checkConnectedStatus } from "./client.js";
2 | import { callMyServer } from "./utilities.js";
3 |
4 | let linkTokenData;
5 |
6 | export const initializeLink = async function () {
7 | linkTokenData = await callMyServer("/server/generate_link_token", true);
8 | document.querySelector("#startLink").classList.remove("opacity-50");
9 | };
10 |
11 | export const startMicroVerification = async function () {
12 | linkTokenData = await callMyServer(
13 | "/server/generate_link_token_for_micros",
14 | true
15 | );
16 | startLink();
17 | };
18 |
19 | const startLink = function () {
20 | if (linkTokenData === undefined) {
21 | return;
22 | }
23 | const handler = Plaid.create({
24 | token: linkTokenData.link_token,
25 | onSuccess: async (publicToken, metadata) => {
26 | console.log(`ONSUCCESS: Metadata ${JSON.stringify(metadata)}`);
27 | console.log(
28 | `I have a public token: ${publicToken} I should exchange this`
29 | );
30 | await exchangeToken(publicToken);
31 | },
32 | onExit: (err, metadata) => {
33 | console.log(
34 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
35 | metadata
36 | )}`
37 | );
38 | },
39 | onEvent: (eventName, metadata) => {
40 | console.log(`Event ${eventName}, Metadata: ${JSON.stringify(metadata)}`);
41 | },
42 | });
43 | handler.open();
44 | };
45 |
46 | async function exchangeToken(publicToken) {
47 | await callMyServer("/server/swap_public_token", true, {
48 | public_token: publicToken,
49 | });
50 | console.log("Done exchanging our token. I'll re-fetch our status");
51 | await checkConnectedStatus();
52 | }
53 |
54 | document.querySelector("#startLink").addEventListener("click", () => {
55 | startLink(false);
56 | });
57 |
--------------------------------------------------------------------------------
/webhooks/finished/public/js/connect.js:
--------------------------------------------------------------------------------
1 | import { checkConnectedStatus } from "./index.js";
2 |
3 | let linkTokenData;
4 |
5 | export const initializeLink = async function () {
6 | const linkTokenResponse = await fetch("/server/generate_link_token", {
7 | method: "POST",
8 | });
9 | linkTokenData = await linkTokenResponse.json();
10 | document.querySelector("#startLink").classList.remove("opacity-50");
11 | console.log(JSON.stringify(linkTokenData));
12 | };
13 |
14 | const startLink = function () {
15 | if (linkTokenData === undefined) {
16 | return;
17 | }
18 | const handler = Plaid.create({
19 | token: linkTokenData.link_token,
20 | onSuccess: async (publicToken, metadata) => {
21 | console.log(
22 | `I have a public token: ${publicToken} I should exchange this`
23 | );
24 | await exchangeToken(publicToken);
25 | },
26 | onExit: (err, metadata) => {
27 | console.log(
28 | `Exited early. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
29 | metadata
30 | )}`
31 | );
32 | },
33 | onEvent: (eventName, metadata) => {
34 | console.log(`Event ${eventName}`);
35 | },
36 | });
37 | handler.open();
38 | };
39 |
40 | async function exchangeToken(publicToken) {
41 | const tokenExchangeResponse = await fetch("/server/swap_public_token", {
42 | method: "POST",
43 | headers: { "Content-type": "application/json" },
44 | body: JSON.stringify({ public_token: publicToken }),
45 | });
46 | // This is where I'd add our error checking... if our server returned any
47 | // errors.
48 | const tokenExchangeData = await tokenExchangeResponse.json();
49 | console.log("Done exchanging our token");
50 | await checkConnectedStatus();
51 | }
52 |
53 | document.querySelector("#startLink").addEventListener("click", startLink);
54 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/public/js/connect.js:
--------------------------------------------------------------------------------
1 | let linkTokenData;
2 |
3 | const initializeLink = async function () {
4 | const linkTokenResponse = await fetch(`/api/create_link_token`);
5 | linkTokenData = await linkTokenResponse.json();
6 | localStorage.setItem("linkTokenData", JSON.stringify(linkTokenData));
7 | document.querySelector("#startLink").classList.remove("opacity-50");
8 | console.log(JSON.stringify(linkTokenData));
9 | };
10 |
11 | const startLink = function () {
12 | if (linkTokenData === undefined) {
13 | return;
14 | }
15 | const handler = Plaid.create({
16 | token: linkTokenData.link_token,
17 | onSuccess: async (publicToken, metadata) => {
18 | console.log(
19 | `I have a public token: ${publicToken} I should exchange this`
20 | );
21 | await exchangeToken(publicToken);
22 | },
23 | onExit: (err, metadata) => {
24 | console.log(
25 | `I'm all done. Error: ${JSON.stringify(err)} Metadata: ${JSON.stringify(
26 | metadata
27 | )}`
28 | );
29 | },
30 | onEvent: (eventName, metadata) => {
31 | console.log(`Event ${eventName}`);
32 | },
33 | });
34 | handler.open();
35 | };
36 |
37 | async function exchangeToken(publicToken) {
38 | const tokenExchangeResponse = await fetch(`/api/exchange_public_token`, {
39 | method: "POST",
40 | headers: { "Content-type": "application/json" },
41 | body: JSON.stringify({ public_token: publicToken }),
42 | });
43 | // This is where I'd add our error checking... if our server returned any
44 | // errors.
45 | const tokenExchangeData = await tokenExchangeResponse.json();
46 | console.log("Done exchanging our token");
47 | window.location.href = "index.html";
48 | }
49 |
50 | document.querySelector("#startLink").addEventListener("click", startLink);
51 |
52 | initializeLink();
53 |
--------------------------------------------------------------------------------
/transactions/finished/server/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const cookieParser = require("cookie-parser");
5 |
6 | const APP_PORT = process.env.APP_PORT || 8000;
7 |
8 | /**
9 | * Initialization!
10 | */
11 |
12 | // Set up the server
13 |
14 | const app = express();
15 | app.use(cookieParser());
16 | app.use(bodyParser.urlencoded({ extended: false }));
17 | app.use(bodyParser.json());
18 | app.use(express.static("./public"));
19 |
20 | const server = app.listen(APP_PORT, function () {
21 | console.log(`Server is up and running at http://localhost:${APP_PORT}/`);
22 | });
23 |
24 | const usersRouter = require("./routes/users");
25 | const linkTokenRouter = require("./routes/tokens");
26 | const bankRouter = require("./routes/banks");
27 | const { router: transactionsRouter } = require("./routes/transactions");
28 | const debugRouter = require("./routes/debug");
29 | const { getWebhookServer } = require("./webhookServer");
30 |
31 | app.use("/server/users", usersRouter);
32 | app.use("/server/tokens", linkTokenRouter);
33 | app.use("/server/banks", bankRouter);
34 | app.use("/server/transactions", transactionsRouter);
35 | app.use("/server/debug", debugRouter);
36 |
37 | /* Add in some basic error handling so our server doesn't crash if we run into
38 | * an error.
39 | */
40 | const errorHandler = function (err, req, res, next) {
41 | console.error(`Your error:`);
42 | console.error(err);
43 | if (err.response?.data != null) {
44 | res.status(500).send(err.response.data);
45 | } else {
46 | res.status(500).send({
47 | error_code: "OTHER_ERROR",
48 | error_message: "I got some other message on the server.",
49 | });
50 | }
51 | };
52 | app.use(errorHandler);
53 |
54 | // Initialize our webhook server, too.
55 | const webhookServer = getWebhookServer();
56 |
--------------------------------------------------------------------------------
/transactions/start/server/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const cookieParser = require("cookie-parser");
5 |
6 | const APP_PORT = process.env.APP_PORT || 8000;
7 |
8 | /**
9 | * Initialization!
10 | */
11 |
12 | // Set up the server
13 |
14 | const app = express();
15 | app.use(cookieParser());
16 | app.use(bodyParser.urlencoded({ extended: false }));
17 | app.use(bodyParser.json());
18 | app.use(express.static("./public"));
19 |
20 | const server = app.listen(APP_PORT, function () {
21 | console.log(`Server is up and running at http://localhost:${APP_PORT}/`);
22 | });
23 |
24 | const usersRouter = require("./routes/users");
25 | const linkTokenRouter = require("./routes/tokens");
26 | const bankRouter = require("./routes/banks");
27 | const { router: transactionsRouter } = require("./routes/transactions");
28 | const debugRouter = require("./routes/debug");
29 | const { getWebhookServer } = require("./webhookServer");
30 |
31 | app.use("/server/users", usersRouter);
32 | app.use("/server/tokens", linkTokenRouter);
33 | app.use("/server/banks", bankRouter);
34 | app.use("/server/transactions", transactionsRouter);
35 | app.use("/server/debug", debugRouter);
36 |
37 | /* Add in some basic error handling so our server doesn't crash if we run into
38 | * an error.
39 | */
40 | const errorHandler = function (err, req, res, next) {
41 | console.error(`Your error:`);
42 | console.error(err);
43 | if (err.response?.data != null) {
44 | res.status(500).send(err.response.data);
45 | } else {
46 | res.status(500).send({
47 | error_code: "OTHER_ERROR",
48 | error_message: "I got some other message on the server.",
49 | });
50 | }
51 | };
52 | app.use(errorHandler);
53 |
54 | // Initialize our webhook server, too.
55 | const webhookServer = getWebhookServer();
56 |
--------------------------------------------------------------------------------
/transactions/start/server/routes/transactions.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { getLoggedInUserId } = require("../utils");
3 | const db = require("../db");
4 | const { plaidClient } = require("../plaid");
5 | const { setTimeout } = require("timers/promises");
6 | const { SimpleTransaction } = require("../simpleTransactionObject");
7 |
8 | const router = express.Router();
9 |
10 | /**
11 | * This will ask our server to make a transactions sync call
12 | * against all the items it has for a particular user. This is one way
13 | * you can keep your transaction data up to date, but it's preferable
14 | * to just fetch data for a single item in response to a webhook.
15 | */
16 | router.post("/sync", async (req, res, next) => {
17 | try {
18 | res.json({ todo: "Implement this method" });
19 | } catch (error) {
20 | console.log(`Running into an error!`);
21 | next(error);
22 | }
23 | });
24 |
25 | /**
26 | * Given an item ID, this will fetch all transactions for all accounts
27 | * associated with this item using the sync API. We can call this manually
28 | * using the /sync endpoint above, or we can call this in response
29 | * to a webhook
30 | */
31 | const syncTransactions = async function (itemId) {
32 | const summary = { added: 0, removed: 0, modified: 0 };
33 | // TODO: Implement this!
34 | return summary;
35 | };
36 |
37 | /**
38 | * Fetch all the transactions for a particular user (up to a limit)
39 | * This is really just a simple database query, since our server has already
40 | * fetched these items using the syncTransactions call above
41 | *
42 | */
43 | router.get("/list", async (req, res, next) => {
44 | try {
45 | res.json({ todo: "Implement this method" });
46 | } catch (error) {
47 | console.log(`Running into an error!`);
48 | next(error);
49 | }
50 | });
51 |
52 | module.exports = { router, syncTransactions };
53 |
--------------------------------------------------------------------------------
/auth/start/public/js/utilities.js:
--------------------------------------------------------------------------------
1 | export const enableSection = function (sectionId) {
2 | const sectionsToCheck = [
3 | "#authConnected",
4 | "#automatedMicros",
5 | "#sameDayMicros",
6 | ];
7 | sectionsToCheck.forEach((sel) => {
8 | const parentDiv = document.querySelector(sel);
9 | if (sel === sectionId) {
10 | parentDiv.classList.remove("opacity-25");
11 | document.querySelectorAll(sel + " button").forEach((button) => {
12 | button.disabled = false;
13 | });
14 | } else {
15 | parentDiv.classList.add("opacity-25");
16 | document.querySelectorAll(sel + " button").forEach((button) => {
17 | button.disabled = true;
18 | });
19 | }
20 | });
21 | };
22 |
23 | export const callMyServer = async function (
24 | endpoint,
25 | isPost = false,
26 | postData = null
27 | ) {
28 | const optionsObj = isPost ? { method: "POST" } : {};
29 | if (isPost && postData !== null) {
30 | optionsObj.headers = { "Content-type": "application/json" };
31 | optionsObj.body = JSON.stringify(postData);
32 | }
33 | const response = await fetch(endpoint, optionsObj);
34 | if (response.status === 500) {
35 | await handleServerError(response);
36 | return;
37 | }
38 | const data = await response.json();
39 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
40 | return data;
41 | };
42 |
43 | export const showOutput = function (textToShow) {
44 | if (textToShow == null) return;
45 | const output = document.querySelector("#output");
46 | output.textContent = textToShow;
47 | };
48 |
49 | const handleServerError = async function (responseObject) {
50 | const error = await responseObject.json();
51 | console.error("I received an error ", error);
52 | if (error.hasOwnProperty("error_message")) {
53 | showOutput(`Error: ${error.error_message} -- See console for more`);
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/auth/finished/public/js/utilities.js:
--------------------------------------------------------------------------------
1 | export const enableSection = function (sectionId) {
2 | const sectionsToCheck = [
3 | "#authConnected",
4 | "#automatedMicros",
5 | "#sameDayMicros",
6 | ];
7 | sectionsToCheck.forEach((sel) => {
8 | const parentDiv = document.querySelector(sel);
9 | if (sel === sectionId) {
10 | parentDiv.classList.remove("opacity-25");
11 | document.querySelectorAll(sel + " button").forEach((button) => {
12 | button.disabled = false;
13 | });
14 | } else {
15 | parentDiv.classList.add("opacity-25");
16 | document.querySelectorAll(sel + " button").forEach((button) => {
17 | button.disabled = true;
18 | });
19 | }
20 | });
21 | };
22 |
23 | export const callMyServer = async function (
24 | endpoint,
25 | isPost = false,
26 | postData = null
27 | ) {
28 | const optionsObj = isPost ? { method: "POST" } : {};
29 | if (isPost && postData !== null) {
30 | optionsObj.headers = { "Content-type": "application/json" };
31 | optionsObj.body = JSON.stringify(postData);
32 | }
33 | const response = await fetch(endpoint, optionsObj);
34 | if (response.status === 500) {
35 | await handleServerError(response);
36 | return;
37 | }
38 | const data = await response.json();
39 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
40 | return data;
41 | };
42 |
43 | export const showOutput = function (textToShow) {
44 | if (textToShow == null) return;
45 | const output = document.querySelector("#output");
46 | output.textContent = textToShow;
47 | };
48 |
49 | const handleServerError = async function (responseObject) {
50 | const error = await responseObject.json();
51 | console.error("I received an error ", error);
52 | if (error.hasOwnProperty("error_message")) {
53 | showOutput(`Error: ${error.error_message} -- See console for more`);
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tutorial Resources
2 |
3 | This repo contains starter apps, samples, and other resources that you may need to use alongside some of the tutorials provided by Plaid. They are not intended to be used as standalone samples. Please see the [Quickstart](https://github.com/plaid/quickstart), the [Tiny Quickstart](https://github.com/plaid/tiny-quickstart), or the [Plaid Pattern](https://github.com/plaid/pattern) samples if you're looking for working sample apps.
4 |
5 | ## Contents
6 | * [`/vanilla-js-oauth`](https://github.com/plaid/tutorial-resources/tree/main/vanilla-js-oauth) -- A sample application to be used alongside the [Plaid OAuth and JavaScript](https://www.youtube.com/watch?v=E0GwNBFVGik) screencast.
7 | * [`/webhooks`](https://github.com/plaid/tutorial-resources/tree/main/webhooks) -- A sample application to be used alongside the [Plaid Webhooks](https://www.youtube.com/watch?v=0E0KEAVeDyc) tutorial.
8 | * [`/auth`](https://github.com/plaid/tutorial-resources/tree/main/auth) -- A sample application to be used alongside the [Plaid Auth](https://www.youtube.com/watch?v=FlZ5nzlIq74) screencast.
9 | * [`/transactions`](https://github.com/plaid/tutorial-resources/tree/main/transactions) -- A sample application to be used alongside the [Plaid Transactions](https://www.youtube.com/watch?v=hBiKJ6vTa4g) screencast.
10 | * [`/ios-swift`](https://github.com/plaid/tutorial-resources/tree/main/ios-swift) -- A sample application to be used alongside the [Plaid on iOS Tutorial](https://www.youtube.com/watch?v=9fgmW38usTo).
11 |
12 | ## How to use this repository
13 |
14 | Every resource should be contained in its own directory, with a `start` and `finished` folder. You should open up the `start` folder and follow along with the corresponding tutorial.
15 |
16 | The `finished` folder is there if you get really stuck and need to compare your sample to a working copy. We recommend you don't just copy code from the `finished` folder without going through the tutorial -- you won't learn anything, and we will silently judge you for it. :eyes:
17 |
--------------------------------------------------------------------------------
/transactions/start/public/js/signin.js:
--------------------------------------------------------------------------------
1 | import { callMyServer, showSelector, hideSelector, resetUI } from "./utils.js";
2 | import { refreshConnectedBanks, clientRefresh } from "./client.js";
3 | /**
4 | * Methods to handle signing in and creating new users. Because this is just
5 | * a sample, we decided to skip the whole "creating a password" thing.
6 | */
7 |
8 | export const createNewUser = async function () {
9 | const newUsername = document.querySelector("#username").value;
10 | await callMyServer("/server/users/create", true, {
11 | username: newUsername,
12 | });
13 | await refreshSignInStatus();
14 | };
15 |
16 | /**
17 | * Get a list of all of our users on the server.
18 | */
19 | const getExistingUsers = async function () {
20 | const usersList = await callMyServer("/server/users/list");
21 | if (usersList.length === 0) {
22 | hideSelector("#existingUsers");
23 | } else {
24 | showSelector("#existingUsers");
25 | document.querySelector("#existingUsersSelect").innerHTML = usersList.map(
26 | (userObj) => `${userObj.username} `
27 | );
28 | }
29 | };
30 |
31 | export const signIn = async function () {
32 | const userId = document.querySelector("#existingUsersSelect").value;
33 | await callMyServer("/server/users/sign_in", true, { userId: userId });
34 | await refreshSignInStatus();
35 | };
36 |
37 | export const signOut = async function () {
38 | await callMyServer("/server/users/sign_out", true);
39 | await refreshSignInStatus();
40 | resetUI();
41 | };
42 |
43 | export const refreshSignInStatus = async function () {
44 | const userInfoObj = await callMyServer("/server/users/get_my_info");
45 | const userInfo = userInfoObj.userInfo;
46 | if (userInfo == null) {
47 | showSelector("#notSignedIn");
48 | hideSelector("#signedIn");
49 | getExistingUsers();
50 | } else {
51 | showSelector("#signedIn");
52 | hideSelector("#notSignedIn");
53 | document.querySelector("#welcomeMessage").textContent = `Signed in as ${
54 | userInfo.username
55 | } (user ID #${userInfo.id.substr(0, 8)}...)`;
56 | await refreshConnectedBanks();
57 |
58 | await clientRefresh();
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/transactions/finished/public/js/signin.js:
--------------------------------------------------------------------------------
1 | import { callMyServer, showSelector, hideSelector, resetUI } from "./utils.js";
2 | import { refreshConnectedBanks, clientRefresh } from "./client.js";
3 | /**
4 | * Methods to handle signing in and creating new users. Because this is just
5 | * a sample, we decided to skip the whole "creating a password" thing.
6 | */
7 |
8 | export const createNewUser = async function () {
9 | const newUsername = document.querySelector("#username").value;
10 | await callMyServer("/server/users/create", true, {
11 | username: newUsername,
12 | });
13 | await refreshSignInStatus();
14 | };
15 |
16 | /**
17 | * Get a list of all of our users on the server.
18 | */
19 | const getExistingUsers = async function () {
20 | const usersList = await callMyServer("/server/users/list");
21 | if (usersList.length === 0) {
22 | hideSelector("#existingUsers");
23 | } else {
24 | showSelector("#existingUsers");
25 | document.querySelector("#existingUsersSelect").innerHTML = usersList.map(
26 | (userObj) => `${userObj.username} `
27 | );
28 | }
29 | };
30 |
31 | export const signIn = async function () {
32 | const userId = document.querySelector("#existingUsersSelect").value;
33 | await callMyServer("/server/users/sign_in", true, { userId: userId });
34 | await refreshSignInStatus();
35 | };
36 |
37 | export const signOut = async function () {
38 | await callMyServer("/server/users/sign_out", true);
39 | await refreshSignInStatus();
40 | resetUI();
41 | };
42 |
43 | export const refreshSignInStatus = async function () {
44 | const userInfoObj = await callMyServer("/server/users/get_my_info");
45 | const userInfo = userInfoObj.userInfo;
46 | if (userInfo == null) {
47 | showSelector("#notSignedIn");
48 | hideSelector("#signedIn");
49 | getExistingUsers();
50 | } else {
51 | showSelector("#signedIn");
52 | hideSelector("#notSignedIn");
53 | document.querySelector("#welcomeMessage").textContent = `Signed in as ${
54 | userInfo.username
55 | } (user ID #${userInfo.id.substr(0, 8)}...)`;
56 | await refreshConnectedBanks();
57 |
58 | await clientRefresh();
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/InitViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class InitViewController: UIViewController {
11 |
12 | @IBOutlet var userLabel: UILabel!
13 | @IBOutlet var statusLabel: UILabel!
14 | @IBOutlet var simpleCallResults: UILabel!
15 |
16 |
17 | @IBOutlet var connectToPlaid: UIButton!
18 | @IBOutlet var simpleCallButton: UIButton!
19 | let communicator = ServerCommunicator()
20 |
21 | @IBAction func makeSimpleCallWasPressed(_ sender: Any) {
22 | // Ask our server to make a call to the Plaid API on behalf of our user
23 | }
24 |
25 | private func determineUserStatus() {
26 | self.communicator.callMyServer(path: "/server/get_user_info", httpMethod: .get) {
27 | (result: Result) in
28 |
29 | switch result {
30 | case .success(let serverResponse):
31 | self.userLabel.text = "Hello user \(serverResponse.userId)!"
32 | switch serverResponse.userStatus {
33 | case .connected:
34 | self.statusLabel.text = "You are connected to your bank via Plaid. Make a call!"
35 | self.connectToPlaid.setTitle("Make a new connection", for: .normal)
36 | self.simpleCallButton.isEnabled = true;
37 | case .disconnected:
38 | self.statusLabel.text = "You should connect to a bank"
39 | self.connectToPlaid.setTitle("Connect", for: .normal)
40 | self.simpleCallButton.isEnabled = false;
41 | }
42 | self.connectToPlaid.isEnabled = true;
43 | case .failure(let error):
44 | print(error)
45 | }
46 | }
47 | }
48 |
49 | override func viewDidAppear(_ animated: Bool) {
50 | super.viewDidAppear(animated)
51 | // We'll refresh this every time our view appears
52 | determineUserStatus()
53 | }
54 |
55 | override func viewDidLoad() {
56 | super.viewDidLoad()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/README.md:
--------------------------------------------------------------------------------
1 | # Vanilla-js-OAuth
2 |
3 | ### Overview
4 |
5 | This is a starter app that is to be used with the Getting Started with OAuth and JavaScript [YouTube tutorial](https://www.youtube.com/watch?v=E0GwNBFVGik). It implements a very basic version of a Plaid-powered application using HTML/VanillaJS on the front end, and NodeJS/Express on the backend.
6 |
7 | ### Running the app
8 |
9 | If you want the most complete instructions for running the app, please follow along with the video tutorial linked above.
10 |
11 | #### Clone the repo
12 |
13 | Clone the tutorial resources repo to your machine and cd into the project's start directory:
14 |
15 | ```bash
16 | git clone https://github.com/plaid/tutorial-resources && cd vanilla-js-oauth/start/
17 | ```
18 |
19 | #### Set up your environment
20 |
21 | This app uses the latest stable version of Node. At the time of this writing, that's v16.14.0. It's recommended you use a similar version of Node to run the app. For information on installing Node, see [How to install Node.js](https://nodejs.dev/learn/how-to-install-nodejs), and consider using [nvm](https://github.com/nvm-sh/nvm) to easily switch between Node versions.
22 |
23 | #### Install dependencies
24 |
25 | Install the necessary dependencies:
26 |
27 | ```bash
28 | npm install
29 | ```
30 |
31 | #### Equip the app with credentials
32 |
33 | Copy the included **.env.example** to a file called **.env**.
34 |
35 | ```bash
36 | cp .env.example .env
37 | ```
38 |
39 | Fill out the contents of the **.env** file with the [client ID and Sandbox secret in your Plaid dashboard](https://dashboard.plaid.com/team/keys). Don't place quotes around the credentials. Use the "Sandbox" secret when setting the `PLAID_SECRET` variable.
40 |
41 | #### Start the server
42 |
43 | Start the app by running the following command:
44 |
45 | ```bash
46 | npm start
47 | ```
48 |
49 | The server will start running on port 8000. To use the app, navigate to `localhost:8000` in your browser.
50 |
51 | ### Troubleshooting
52 |
53 | #### MISSING_FIELDS error
54 |
55 | If you encounter a **MISSING_FIELDS** error, it's possible you did not properly fill out the **.env** file. Be sure to add your client ID and Sandbox secret to the corresponding variables in the file.
56 |
--------------------------------------------------------------------------------
/webhooks/README.md:
--------------------------------------------------------------------------------
1 | # Webhooks
2 |
3 | ### Overview
4 |
5 | This is a starter app that is to be used with the Plaid and Webhooks [YouTube tutorial](https://www.youtube.com/). It implements a very basic version of a Plaid-powered application using HTML/VanillaJS on the front end, and NodeJS/Express on the backend.
6 |
7 | ### Running the app
8 |
9 | If you want the most complete instructions for running the app, please follow along with the video tutorial linked above.
10 |
11 | #### Clone the repo
12 |
13 | Clone the tutorial resources repo to your machine and cd into the project's start directory:
14 |
15 | ```bash
16 | git clone https://github.com/plaid/tutorial-resources && cd webhooks/start/
17 | ```
18 |
19 | #### Set up your environment
20 |
21 | This app uses the latest stable version of Node. At the time of this writing, that's v16.14.2. It's recommended you use a similar version of Node to run the app. For information on installing Node, see [How to install Node.js](https://nodejs.dev/learn/how-to-install-nodejs), and consider using [nvm](https://github.com/nvm-sh/nvm) to easily switch between Node versions.
22 |
23 | #### Install dependencies
24 |
25 | Install the necessary dependencies:
26 |
27 | ```bash
28 | npm install
29 | ```
30 |
31 | #### Equip the app with credentials
32 |
33 | Copy the included **.env.example** to a file called **.env**.
34 |
35 | ```bash
36 | cp .env.template .env
37 | ```
38 |
39 | Fill out the contents of the **.env** file with the [client ID and Sandbox secret in your Plaid dashboard](https://dashboard.plaid.com/team/keys). Don't place quotes around the credentials. Use the "Sandbox" secret when setting the `PLAID_SECRET` variable.
40 |
41 | #### Start the server
42 |
43 | Start the app by running the following command:
44 |
45 | ```bash
46 | npm run watch
47 | ```
48 |
49 | The server will start running on port 8000. To use the app, navigate to `localhost:8000` in your browser.
50 |
51 | The application will create a `user_data.json` file to store information about your current user, like their access token. Delete this file (and restart the server) if you want to start over again with a new user.
52 |
53 | ### Troubleshooting
54 |
55 | #### MISSING_FIELDS error
56 |
57 | If you encounter a **MISSING_FIELDS** error, it's possible you did not properly fill out the **.env** file. Be sure to add your client ID and Sandbox secret to the corresponding variables in the file.
58 |
--------------------------------------------------------------------------------
/ios-swift/README.md:
--------------------------------------------------------------------------------
1 | # iOS Starter App
2 |
3 | ### Overview
4 |
5 | This is a starter app that is intended to be used with the Getting Started with iOS [YouTube tutorial](https://www.youtube.com/watch?v=9fgmW38usTo). It implements a very basic app that allows a user to connect to their bank via Plaid, and then makes a simple call to /auth/get to retrieve the user's routing number. This application uses Swift on the front end, and NodeJS/Express on the backend.
6 |
7 | ### Running the app
8 |
9 | If you want the most complete instructions for running the app, please follow along with the video tutorial linked above. The following abbreviated instructions are for those of you who really don't want to watch videos.
10 |
11 | #### Clone the repo
12 |
13 | Clone the tutorial resources repo to your machine and cd into the project's start directory:
14 |
15 | ```bash
16 | git clone https://github.com/plaid/tutorial-resources && cd ios/start
17 | ```
18 |
19 | ### Set up your server
20 |
21 | The server is designed to be used with Node 16 or higher. For information on installing Node, see [How to install Node.js](https://nodejs.dev/learn/how-to-install-nodejs), and consider using [nvm](https://github.com/nvm-sh/nvm) to easily switch between Node versions.
22 |
23 | #### Move into your server directory
24 |
25 | ```bash
26 | cd server
27 | ```
28 |
29 | #### Install dependencies
30 |
31 | Install the necessary dependencies:
32 |
33 | ```bash
34 | npm install
35 | ```
36 |
37 | #### Add your credentials to the app
38 |
39 | Copy the included **.env.template** to a file called **.env**.
40 |
41 | ```bash
42 | cp .env.template .env
43 | ```
44 |
45 | Fill out the contents of the **.env** file with the [client ID and secret in your Plaid dashboard](https://dashboard.plaid.com/team/keys). Make sure to pick the appropriate secret value for the environment you're using. (We recommend using sandbox to start.)
46 |
47 | Also, add a user ID to represent the current "logged in" user.
48 |
49 | #### Start the server
50 |
51 | Start the app by running the following command:
52 |
53 | ```bash
54 | npm run watch
55 | ```
56 |
57 | The server will start running on port 8000 and will update whenever you make a change to the server files.
58 |
59 | ### Set up your client
60 |
61 | - Open up `SamplePlaidClient.xcodeproj` in Xcode
62 | - In the `SamplePlaidClient` target, change the bundle identifier to something appropriate for your organization
63 | - That's it! You should be able to run your project in the simulator.
64 |
--------------------------------------------------------------------------------
/transactions/finished/public/js/utils.js:
--------------------------------------------------------------------------------
1 | export const callMyServer = async function (
2 | endpoint,
3 | isPost = false,
4 | postData = null
5 | ) {
6 | const optionsObj = isPost ? { method: "POST" } : {};
7 | if (isPost && postData !== null) {
8 | optionsObj.headers = { "Content-type": "application/json" };
9 | optionsObj.body = JSON.stringify(postData);
10 | }
11 | const response = await fetch(endpoint, optionsObj);
12 | if (response.status === 500) {
13 | await handleServerError(response);
14 | return;
15 | }
16 | const data = await response.json();
17 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
18 | return data;
19 | };
20 |
21 | export const hideSelector = function (selector) {
22 | document.querySelector(selector).classList.add("d-none");
23 | };
24 |
25 | export const showSelector = function (selector) {
26 | document.querySelector(selector).classList.remove("d-none");
27 | };
28 |
29 | export const showOutput = function (textToShow) {
30 | if (textToShow == null) return;
31 | const output = document.querySelector("#output");
32 | output.textContent = textToShow;
33 | };
34 |
35 | export const humanReadableCategory = function (category) {
36 | return category
37 | .replace(/_/g, " ")
38 | .toLowerCase()
39 | .replace(/\b\w/g, (s) => s.toUpperCase())
40 | .replace(/\b(And|Or)\b/, (s) => s.toLowerCase());
41 | };
42 |
43 | const formatters = {
44 | USD: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }),
45 | };
46 |
47 | export const currencyAmount = function (amount, currencyCode) {
48 | try {
49 | // Create a new formatter if this doesn't exist
50 | if (formatters[currencyCode] == null) {
51 | formatters[currencyCode] = new Intl.NumberFormat("en-US", {
52 | style: "currency",
53 | currency: currencyCode,
54 | });
55 | }
56 | return formatters[currencyCode].format(amount);
57 | } catch (error) {
58 | console.log(error);
59 | return amount;
60 | }
61 | };
62 |
63 | const handleServerError = async function (responseObject) {
64 | const error = await responseObject.json();
65 | console.error("I received an error ", error);
66 | if (error.hasOwnProperty("error_message")) {
67 | showOutput(`Error: ${error.error_message} -- See console for more`);
68 | }
69 | };
70 |
71 | export const resetUI = function () {
72 | showOutput("");
73 | document.querySelector("#username").value = "";
74 | document.querySelector("#email").value = "";
75 | };
76 |
--------------------------------------------------------------------------------
/transactions/start/public/js/utils.js:
--------------------------------------------------------------------------------
1 | export const callMyServer = async function (
2 | endpoint,
3 | isPost = false,
4 | postData = null
5 | ) {
6 | const optionsObj = isPost ? { method: "POST" } : {};
7 | if (isPost && postData !== null) {
8 | optionsObj.headers = { "Content-type": "application/json" };
9 | optionsObj.body = JSON.stringify(postData);
10 | }
11 | const response = await fetch(endpoint, optionsObj);
12 | if (response.status === 500) {
13 | await handleServerError(response);
14 | return;
15 | }
16 | const data = await response.json();
17 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
18 | return data;
19 | };
20 |
21 | export const hideSelector = function (selector) {
22 | document.querySelector(selector).classList.add("d-none");
23 | };
24 |
25 | export const showSelector = function (selector) {
26 | document.querySelector(selector).classList.remove("d-none");
27 | };
28 |
29 | export const showOutput = function (textToShow) {
30 | if (textToShow == null) return;
31 | const output = document.querySelector("#output");
32 | output.textContent = textToShow;
33 | };
34 |
35 | export const humanReadableCategory = function (category) {
36 | return category
37 | .replace(/_/g, " ")
38 | .toLowerCase()
39 | .replace(/\b\w/g, (s) => s.toUpperCase())
40 | .replace(/\b(And|Or)\b/, (s) => s.toLowerCase());
41 | };
42 |
43 | const formatters = {
44 | USD: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }),
45 | };
46 |
47 | export const currencyAmount = function (amount, currencyCode) {
48 | try {
49 | // Create a new formatter if this doesn't exist
50 | if (formatters[currencyCode] == null) {
51 | formatters[currencyCode] = new Intl.NumberFormat("en-US", {
52 | style: "currency",
53 | currency: currencyCode,
54 | });
55 | }
56 | return formatters[currencyCode].format(amount);
57 | } catch (error) {
58 | console.log(error);
59 | return amount;
60 | }
61 | };
62 |
63 | const handleServerError = async function (responseObject) {
64 | const error = await responseObject.json();
65 | console.error("I received an error ", error);
66 | if (error.hasOwnProperty("error_message")) {
67 | showOutput(`Error: ${error.error_message} -- See console for more`);
68 | }
69 | };
70 |
71 | export const resetUI = function () {
72 | showOutput("");
73 | document.querySelector("#username").value = "";
74 | document.querySelector("#email").value = "";
75 | };
76 |
--------------------------------------------------------------------------------
/auth/start/public/js/client.js:
--------------------------------------------------------------------------------
1 | import { initializeLink, startMicroVerification } from "./connect.js";
2 | import { enableSection, callMyServer, showOutput } from "./utilities.js";
3 |
4 | export const checkConnectedStatus = async function () {
5 | const connectedData = await callMyServer("/server/get_user_info");
6 | if (connectedData.user_status === "connected") {
7 | document.querySelector("#connectedUI").classList.remove("d-none");
8 | document.querySelector("#disconnectedUI").classList.add("d-none");
9 | enableSection("");
10 | } else {
11 | document.querySelector("#connectedUI").classList.add("d-none");
12 | document.querySelector("#disconnectedUI").classList.remove("d-none");
13 | initializeLink();
14 | }
15 | };
16 |
17 | const getProcessorToken = async function () {
18 | // TODO: Fill this out
19 | };
20 |
21 | const getAccountStatus = async function () {
22 | const accountData = await callMyServer("/server/get_account_status");
23 | showOutput(JSON.stringify(accountData));
24 | };
25 |
26 | const refreshUserStatus = async function () {
27 | const updatedData = await callMyServer("/server/get_user_info");
28 | };
29 |
30 | const displayAuthDetails = function (authStatus) {
31 | // TODO: Fill this out
32 | };
33 |
34 | const refreshAuth = async function () {
35 | // TODO: Fill this out
36 | };
37 |
38 | const forceAutoDeposits = async function () {
39 | // TODO: Fill this out
40 | };
41 |
42 | const getAuthInfo = async function () {
43 | // TODO: Fill this out
44 | };
45 |
46 | const verifyMicros = async function () {
47 | // TODO: Fill this out
48 | };
49 |
50 | const postPendingMicros = async function () {
51 | // TODO: Fill this out
52 | };
53 |
54 | const checkForMicros = async function () {
55 | // TODO: Fill this out
56 | };
57 |
58 | // Connect selectors to functions
59 | const selectorsAndFunctions = {
60 | "#getAuthInfo": getAuthInfo,
61 | "#getProcessorToken": getProcessorToken,
62 | "#getAccountStatus": getAccountStatus,
63 | "#refreshAuthStatus": refreshUserStatus,
64 | "#serverRefresh": refreshAuth,
65 | "#impatient": forceAutoDeposits,
66 | "#verifyMicros": verifyMicros,
67 | "#postPendingMicros": postPendingMicros,
68 | "#checkForMicros": checkForMicros,
69 | };
70 |
71 | Object.entries(selectorsAndFunctions).forEach(([sel, fun]) => {
72 | if (document.querySelector(sel) == null) {
73 | console.warn(`Hmm... couldn't find ${sel}`);
74 | } else {
75 | document.querySelector(sel)?.addEventListener("click", fun);
76 | }
77 | });
78 | checkConnectedStatus();
79 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
16 | // Called when we're directed here by a Universal Link
17 | }
18 |
19 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
20 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
21 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
22 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
23 | guard let _ = (scene as? UIWindowScene) else { return }
24 | }
25 |
26 | func sceneDidDisconnect(_ scene: UIScene) {
27 | // Called as the scene is being released by the system.
28 | // This occurs shortly after the scene enters the background, or when its session is discarded.
29 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
30 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
31 | }
32 |
33 | func sceneDidBecomeActive(_ scene: UIScene) {
34 | // Called when the scene has moved from an inactive state to an active state.
35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
36 | }
37 |
38 | func sceneWillResignActive(_ scene: UIScene) {
39 | // Called when the scene will move from an active state to an inactive state.
40 | // This may occur due to temporary interruptions (ex. an incoming phone call).
41 | }
42 |
43 | func sceneWillEnterForeground(_ scene: UIScene) {
44 | // Called as the scene transitions from the background to the foreground.
45 | // Use this method to undo the changes made on entering the background.
46 | }
47 |
48 | func sceneDidEnterBackground(_ scene: UIScene) {
49 | // Called as the scene transitions from the foreground to the background.
50 | // Use this method to save data, release shared resources, and store enough scene-specific state information
51 | // to restore the scene back to its current state.
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
16 | // Called when we're directed here by a Universal Link
17 | }
18 |
19 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
20 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
21 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
22 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
23 | guard let _ = (scene as? UIWindowScene) else { return }
24 | }
25 |
26 | func sceneDidDisconnect(_ scene: UIScene) {
27 | // Called as the scene is being released by the system.
28 | // This occurs shortly after the scene enters the background, or when its session is discarded.
29 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
30 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
31 | }
32 |
33 | func sceneDidBecomeActive(_ scene: UIScene) {
34 | // Called when the scene has moved from an inactive state to an active state.
35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
36 | }
37 |
38 | func sceneWillResignActive(_ scene: UIScene) {
39 | // Called when the scene will move from an active state to an inactive state.
40 | // This may occur due to temporary interruptions (ex. an incoming phone call).
41 | }
42 |
43 | func sceneWillEnterForeground(_ scene: UIScene) {
44 | // Called as the scene transitions from the background to the foreground.
45 | // Use this method to undo the changes made on entering the background.
46 | }
47 |
48 | func sceneDidEnterBackground(_ scene: UIScene) {
49 | // Called as the scene transitions from the foreground to the background.
50 | // Use this method to save data, release shared resources, and store enough scene-specific state information
51 | // to restore the scene back to its current state.
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/transactions/start/server/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const escape = require("escape-html");
3 | const { v4: uuidv4 } = require("uuid");
4 | const { getLoggedInUserId } = require("../utils");
5 | const db = require("../db");
6 |
7 | const router = express.Router();
8 |
9 | /**
10 | * Methods and endpoints for signing in, signing out, and creating new users.
11 | * For the purpose of this sample, we're simply setting / fetching a cookie that
12 | * contains the userID as our way of getting the ID of our signed-in user.
13 | */
14 | router.post("/create", async (req, res, next) => {
15 | try {
16 | const username = escape(req.body.username);
17 | const userId = uuidv4();
18 | const result = await db.addUser(userId, username);
19 | console.log(`User creation result is ${JSON.stringify(result)}`);
20 | if (result["lastID"] != null) {
21 | res.cookie("signedInUser", userId, {
22 | maxAge: 1000 * 60 * 60 * 24 * 30,
23 | httpOnly: true,
24 | });
25 | }
26 | res.json(result);
27 | } catch (error) {
28 | next(error);
29 | }
30 | });
31 |
32 | router.get("/list", async (req, res, next) => {
33 | try {
34 | const result = await db.getUserList();
35 | res.json(result);
36 | } catch (error) {
37 | next(error);
38 | }
39 | });
40 |
41 | router.post("/sign_in", async (req, res, next) => {
42 | try {
43 | const userId = escape(req.body.userId);
44 | res.cookie("signedInUser", userId, {
45 | maxAge: 1000 * 60 * 60 * 24 * 30,
46 | httpOnly: true,
47 | });
48 | res.json({ signedIn: true });
49 | } catch (error) {
50 | next(error);
51 | }
52 | });
53 |
54 | router.post("/sign_out", async (req, res, next) => {
55 | try {
56 | res.clearCookie("signedInUser");
57 | res.json({ signedOut: true });
58 | } catch (error) {
59 | next(error);
60 | }
61 | });
62 |
63 | /**
64 | * Get the id and username of our currently logged in user, if any.
65 | */
66 | router.get("/get_my_info", async (req, res, next) => {
67 | try {
68 | const userId = getLoggedInUserId(req);
69 | console.log(`Your userID is ${userId}`);
70 | let result;
71 | if (userId != null) {
72 | const userObject = await db.getUserRecord(userId);
73 | if (userObject == null) {
74 | // This probably means your cookies are messed up.
75 | res.clearCookie("signedInUser");
76 | res.json({ userInfo: null });
77 | return;
78 | } else {
79 | result = { id: userObject.id, username: userObject.username };
80 | }
81 | } else {
82 | result = null;
83 | }
84 | res.json({ userInfo: result });
85 | } catch (error) {
86 | next(error);
87 | }
88 | });
89 |
90 | module.exports = router;
91 |
--------------------------------------------------------------------------------
/transactions/finished/server/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const escape = require("escape-html");
3 | const { v4: uuidv4 } = require("uuid");
4 | const { getLoggedInUserId } = require("../utils");
5 | const db = require("../db");
6 |
7 | const router = express.Router();
8 |
9 | /**
10 | * Methods and endpoints for signing in, signing out, and creating new users.
11 | * For the purpose of this sample, we're simply setting / fetching a cookie that
12 | * contains the userID as our way of getting the ID of our signed-in user.
13 | */
14 | router.post("/create", async (req, res, next) => {
15 | try {
16 | const username = escape(req.body.username);
17 | const userId = uuidv4();
18 | const result = await db.addUser(userId, username);
19 | console.log(`User creation result is ${JSON.stringify(result)}`);
20 | if (result["lastID"] != null) {
21 | res.cookie("signedInUser", userId, {
22 | maxAge: 1000 * 60 * 60 * 24 * 30,
23 | httpOnly: true,
24 | });
25 | }
26 | res.json(result);
27 | } catch (error) {
28 | next(error);
29 | }
30 | });
31 |
32 | router.get("/list", async (req, res, next) => {
33 | try {
34 | const result = await db.getUserList();
35 | res.json(result);
36 | } catch (error) {
37 | next(error);
38 | }
39 | });
40 |
41 | router.post("/sign_in", async (req, res, next) => {
42 | try {
43 | const userId = escape(req.body.userId);
44 | res.cookie("signedInUser", userId, {
45 | maxAge: 1000 * 60 * 60 * 24 * 30,
46 | httpOnly: true,
47 | });
48 | res.json({ signedIn: true });
49 | } catch (error) {
50 | next(error);
51 | }
52 | });
53 |
54 | router.post("/sign_out", async (req, res, next) => {
55 | try {
56 | res.clearCookie("signedInUser");
57 | res.json({ signedOut: true });
58 | } catch (error) {
59 | next(error);
60 | }
61 | });
62 |
63 | /**
64 | * Get the id and username of our currently logged in user, if any.
65 | */
66 | router.get("/get_my_info", async (req, res, next) => {
67 | try {
68 | const userId = getLoggedInUserId(req);
69 | console.log(`Your userID is ${userId}`);
70 | let result;
71 | if (userId != null) {
72 | const userObject = await db.getUserRecord(userId);
73 | if (userObject == null) {
74 | // This probably means your cookies are messed up.
75 | res.clearCookie("signedInUser");
76 | res.json({ userInfo: null });
77 | return;
78 | } else {
79 | result = { id: userObject.id, username: userObject.username };
80 | }
81 | } else {
82 | result = null;
83 | }
84 | res.json({ userInfo: result });
85 | } catch (error) {
86 | next(error);
87 | }
88 | });
89 |
90 | module.exports = router;
91 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/InitViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/17/23.
6 | //
7 |
8 | import UIKit
9 |
10 | class InitViewController: UIViewController {
11 |
12 | @IBOutlet var userLabel: UILabel!
13 | @IBOutlet var statusLabel: UILabel!
14 | @IBOutlet var simpleCallResults: UILabel!
15 |
16 |
17 | @IBOutlet var connectToPlaid: UIButton!
18 | @IBOutlet var simpleCallButton: UIButton!
19 | let communicator = ServerCommunicator()
20 |
21 | @IBAction func makeSimpleCallWasPressed(_ sender: Any) {
22 | // Ask our server to make a call to the Plaid API on behalf of our user
23 | self.communicator.callMyServer(path: "/server/simple_auth", httpMethod: .get) { (result: Result) in
24 | switch result {
25 | case .success(let response):
26 | self.simpleCallResults.text = "I retrieved routing number \(response.routingNumber) for \(response.accountName) (xxxxxxxxx\(response.accountMask))"
27 | case .failure(let error):
28 | print("Got an error \(error)")
29 | }
30 | }
31 | }
32 |
33 | private func determineUserStatus() {
34 | self.communicator.callMyServer(path: "/server/get_user_info", httpMethod: .get) {
35 | (result: Result) in
36 |
37 | switch result {
38 | case .success(let serverResponse):
39 | self.userLabel.text = "Hello user \(serverResponse.userId)!"
40 | switch serverResponse.userStatus {
41 | case .connected:
42 | self.statusLabel.text = "You are connected to your bank via Plaid. Make a call!"
43 | self.connectToPlaid.setTitle("Make a new connection", for: .normal)
44 | self.simpleCallButton.isEnabled = true;
45 | case .disconnected:
46 | self.statusLabel.text = "You should connect to a bank"
47 | self.connectToPlaid.setTitle("Connect", for: .normal)
48 | self.simpleCallButton.isEnabled = false;
49 | }
50 | self.connectToPlaid.isEnabled = true;
51 | case .failure(let error):
52 | print(error)
53 | }
54 | }
55 | }
56 |
57 | override func viewDidAppear(_ animated: Bool) {
58 | super.viewDidAppear(animated)
59 | // We'll refresh this every time our view appears
60 | determineUserStatus()
61 | }
62 |
63 | override func viewDidLoad() {
64 | super.viewDidLoad()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ios-swift/start/server/user_utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Just a bunch of helper functions that handle reading and writing files
3 | * to represent our "logged in" user. In a real server, this would be writing
4 | * to a database and determining the user's identity using some actual auth.
5 | */
6 |
7 | const fs = require("fs/promises");
8 | const {
9 | FIELD_ACCESS_TOKEN,
10 | FIELD_USER_ID,
11 | FIELD_USER_STATUS,
12 | FIELD_ITEM_ID,
13 | } = require("./constants");
14 |
15 | const USER_FILES_FOLDER = "user_files";
16 | const CURR_USER_ID = process.env.USER_ID || "1";
17 |
18 | /**
19 | * Try to retrieve the user record from our local filesystem and return it
20 | * as a JSON object
21 | */
22 | const _fetchLocalFile = async function () {
23 | const userDataFile = `${USER_FILES_FOLDER}/user_data_${CURR_USER_ID}.json`;
24 | try {
25 | const userData = await fs.readFile(userDataFile, {
26 | encoding: "utf8",
27 | });
28 | const userDataObj = await JSON.parse(userData);
29 | console.log(`Retrieved userData ${userData}`);
30 | return userDataObj;
31 | } catch (error) {
32 | if (error.code === "ENOENT") {
33 | console.log("No user object found. We'll make one from scratch.");
34 | return null;
35 | }
36 | // Might happen first time, if file doesn't exist
37 | console.log("Got an error", error);
38 | return null;
39 | }
40 | };
41 |
42 | const getUserRecord = async function () {
43 | // Attempt to read in our "logged-in user" by looking for a file in the
44 | // "user_files" folder.
45 | let userRecord = await _fetchLocalFile();
46 | if (userRecord == null) {
47 | userRecord = {};
48 | userRecord[FIELD_ACCESS_TOKEN] = null;
49 | userRecord[FIELD_USER_ID] = CURR_USER_ID;
50 | userRecord[FIELD_USER_STATUS] = "disconnected";
51 | userRecord[FIELD_ITEM_ID] = null;
52 |
53 | // Force a file save
54 | await _writeUserRecordToFile(userRecord);
55 | }
56 | return userRecord;
57 | };
58 |
59 | const _writeUserRecordToFile = async function (userRecord) {
60 | const userDataFile = `${USER_FILES_FOLDER}/user_data_${CURR_USER_ID}.json`;
61 | try {
62 | const dataToWrite = JSON.stringify(userRecord);
63 | await fs.writeFile(userDataFile, dataToWrite, {
64 | encoding: "utf8",
65 | mode: 0o600,
66 | });
67 | console.log(`User record ${dataToWrite} written to file.`);
68 | } catch (error) {
69 | console.log("Got an error: ", error);
70 | }
71 | };
72 |
73 | /**
74 | * Updates the user record in memory and writes it to a file. In a real
75 | * application, you'd be writing to a database.
76 | */
77 | const updateUserRecord = async function (keyValDictionary) {
78 | let userRecord = await getUserRecord();
79 | userRecord = { ...userRecord, ...keyValDictionary };
80 | await _writeUserRecordToFile(userRecord);
81 | };
82 |
83 | module.exports = {
84 | getUserRecord,
85 | updateUserRecord,
86 | };
87 |
--------------------------------------------------------------------------------
/ios-swift/finished/server/user_utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Just a bunch of helper functions that handle reading and writing files
3 | * to represent our "logged in" user. In a real server, this would be writing
4 | * to a database and determining the user's identity using some actual auth.
5 | */
6 |
7 | const fs = require("fs/promises");
8 | const {
9 | FIELD_ACCESS_TOKEN,
10 | FIELD_USER_ID,
11 | FIELD_USER_STATUS,
12 | FIELD_ITEM_ID,
13 | } = require("./constants");
14 |
15 | const USER_FILES_FOLDER = "user_files";
16 | const CURR_USER_ID = process.env.USER_ID || "1";
17 |
18 | /**
19 | * Try to retrieve the user record from our local filesystem and return it
20 | * as a JSON object
21 | */
22 | const _fetchLocalFile = async function () {
23 | const userDataFile = `${USER_FILES_FOLDER}/user_data_${CURR_USER_ID}.json`;
24 | try {
25 | const userData = await fs.readFile(userDataFile, {
26 | encoding: "utf8",
27 | });
28 | const userDataObj = await JSON.parse(userData);
29 | console.log(`Retrieved userData ${userData}`);
30 | return userDataObj;
31 | } catch (error) {
32 | if (error.code === "ENOENT") {
33 | console.log("No user object found. We'll make one from scratch.");
34 | return null;
35 | }
36 | // Might happen first time, if file doesn't exist
37 | console.log("Got an error", error);
38 | return null;
39 | }
40 | };
41 |
42 | const getUserRecord = async function () {
43 | // Attempt to read in our "logged-in user" by looking for a file in the
44 | // "user_files" folder.
45 | let userRecord = await _fetchLocalFile();
46 | if (userRecord == null) {
47 | userRecord = {};
48 | userRecord[FIELD_ACCESS_TOKEN] = null;
49 | userRecord[FIELD_USER_ID] = CURR_USER_ID;
50 | userRecord[FIELD_USER_STATUS] = "disconnected";
51 | userRecord[FIELD_ITEM_ID] = null;
52 |
53 | // Force a file save
54 | await _writeUserRecordToFile(userRecord);
55 | }
56 | return userRecord;
57 | };
58 |
59 | const _writeUserRecordToFile = async function (userRecord) {
60 | const userDataFile = `${USER_FILES_FOLDER}/user_data_${CURR_USER_ID}.json`;
61 | try {
62 | const dataToWrite = JSON.stringify(userRecord);
63 | await fs.writeFile(userDataFile, dataToWrite, {
64 | encoding: "utf8",
65 | mode: 0o600,
66 | });
67 | console.log(`User record ${dataToWrite} written to file.`);
68 | } catch (error) {
69 | console.log("Got an error: ", error);
70 | }
71 | };
72 |
73 | /**
74 | * Updates the user record in memory and writes it to a file. In a real
75 | * application, you'd be writing to a database.
76 | */
77 | const updateUserRecord = async function (keyValDictionary) {
78 | let userRecord = await getUserRecord();
79 | userRecord = { ...userRecord, ...keyValDictionary };
80 | await _writeUserRecordToFile(userRecord);
81 | };
82 |
83 | module.exports = {
84 | getUserRecord,
85 | updateUserRecord,
86 | };
87 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient.xcodeproj/xcshareddata/xcschemes/FirstTryPlaid.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient.xcodeproj/xcshareddata/xcschemes/FirstTryPlaid.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/transactions/finished/server/routes/tokens.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const escape = require("escape-html");
3 | const { getLoggedInUserId } = require("../utils");
4 | const db = require("../db");
5 | const { plaidClient } = require("../plaid");
6 | const { syncTransactions } = require("./transactions");
7 |
8 | const router = express.Router();
9 |
10 | const WEBHOOK_URL =
11 | process.env.WEBHOOK_URL || "https://www.example.com/server/receive_webhook";
12 |
13 | /**
14 | * Generates a link token to be used by the client
15 | */
16 | router.post("/generate_link_token", async (req, res, next) => {
17 | try {
18 | const userId = getLoggedInUserId(req);
19 | const userObject = { client_user_id: userId };
20 | const tokenResponse = await plaidClient.linkTokenCreate({
21 | user: userObject,
22 | products: ["transactions"],
23 | client_name: "Where'd My Money Go?",
24 | language: "en",
25 | country_codes: ["US"],
26 | webhook: WEBHOOK_URL,
27 | });
28 | res.json(tokenResponse.data);
29 | } catch (error) {
30 | console.log(`Running into an error!`);
31 | next(error);
32 | }
33 | });
34 |
35 | /**
36 | * Exchanges a public token for an access token. Then, fetches a bunch of
37 | * information about that item and stores it in our database
38 | */
39 | router.post("/exchange_public_token", async (req, res, next) => {
40 | try {
41 | const userId = getLoggedInUserId(req);
42 | const publicToken = escape(req.body.publicToken);
43 |
44 | const tokenResponse = await plaidClient.itemPublicTokenExchange({
45 | public_token: publicToken,
46 | });
47 | const tokenData = tokenResponse.data;
48 | await db.addItem(tokenData.item_id, userId, tokenData.access_token);
49 | await populateBankName(tokenData.item_id, tokenData.access_token);
50 | await populateAccountNames(tokenData.access_token);
51 |
52 | // Call sync for the first time to activate the sync webhooks
53 | await syncTransactions(tokenData.item_id);
54 |
55 | res.json({ status: "success" });
56 | } catch (error) {
57 | console.log(`Running into an error!`);
58 | next(error);
59 | }
60 | });
61 |
62 | const populateBankName = async (itemId, accessToken) => {
63 | try {
64 | const itemResponse = await plaidClient.itemGet({
65 | access_token: accessToken,
66 | });
67 | const institutionId = itemResponse.data.item.institution_id;
68 | if (institutionId == null) {
69 | return;
70 | }
71 | const institutionResponse = await plaidClient.institutionsGetById({
72 | institution_id: institutionId,
73 | country_codes: ["US"],
74 | });
75 | const institutionName = institutionResponse.data.institution.name;
76 | await db.addBankNameForItem(itemId, institutionName);
77 | } catch (error) {
78 | console.log(`Ran into an error! ${error}`);
79 | }
80 | };
81 |
82 | const populateAccountNames = async (accessToken) => {
83 | try {
84 | const acctsResponse = await plaidClient.accountsGet({
85 | access_token: accessToken,
86 | });
87 | const acctsData = acctsResponse.data;
88 | const itemId = acctsData.item.item_id;
89 | await Promise.all(
90 | acctsData.accounts.map(async (acct) => {
91 | await db.addAccount(acct.account_id, itemId, acct.name);
92 | })
93 | );
94 | } catch (error) {
95 | console.log(`Ran into an error! ${error}`);
96 | }
97 | };
98 |
99 | module.exports = router;
100 |
--------------------------------------------------------------------------------
/webhooks/finished/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Webhook tester
12 |
13 |
14 |
15 |
16 |
Welcome!
17 |
18 |
Say, you should connect to your bank to continue.
19 |
Connect my bank
20 |
21 |
22 |
You're connected!
23 |
24 |
25 | Get account balances
26 |
27 |
28 |
29 | Get transactions
30 |
31 |
32 |
33 | Create an asset report
34 |
35 |
36 |
37 |
38 |
44 |
46 |
47 |
48 |
49 | Simulate a webhook
50 |
51 |
52 |
Update my webhook URL
53 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/webhooks/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Webhook tester
12 |
13 |
14 |
15 |
16 |
Welcome!
17 |
18 |
Say, you should connect to your bank to continue.
19 |
Connect my bank
20 |
21 |
22 |
You're connected!
23 |
24 |
25 | Get account balances
26 |
27 |
28 |
29 | Get transactions
30 |
31 |
32 |
33 | Create an asset report
34 |
35 |
36 |
37 |
38 |
44 |
46 |
47 |
48 |
49 | Simulate a webhook
50 |
51 |
52 |
Update my webhook URL
53 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/PlaidLinkViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaidLinkViewController.swift
3 | // SamplePlaidClients
4 | //
5 | // Created by Todd Kerpelman on 8/18/23.
6 | //
7 |
8 | import UIKit
9 | import LinkKit
10 |
11 | class PlaidLinkViewController: UIViewController {
12 | @IBOutlet var startLinkButton: UIButton!
13 | let communicator = ServerCommunicator()
14 | var linkToken: String?
15 | var handler: Handler?
16 |
17 |
18 | private func createLinkConfiguration(linkToken: String) -> LinkTokenConfiguration {
19 | var linkTokenConfig = LinkTokenConfiguration(token: linkToken) { success in
20 | print("Link was finished successfully! \(success)")
21 | self.exchangePublicTokenForAccessToken(success.publicToken)
22 | }
23 | linkTokenConfig.onExit = { linkEvent in
24 | print("User exited link early. \(linkEvent)")
25 | }
26 | linkTokenConfig.onEvent = { linkExit in
27 | print("Hit an event \(linkExit.eventName)")
28 | }
29 | return linkTokenConfig
30 | }
31 |
32 | @IBAction func startLinkWasPressed(_ sender: Any) {
33 | // Handle the button being clicked
34 | guard let linkToken = linkToken else { return }
35 | let config = createLinkConfiguration(linkToken: linkToken)
36 | let creationResult = Plaid.create(config)
37 | switch creationResult {
38 | case .success(let handler):
39 | self.handler = handler
40 |
41 | handler.open(presentUsing: .viewController(self))
42 | case .failure(let error):
43 | print("Handler creation error\(error)")
44 | }
45 | }
46 |
47 | private func exchangePublicTokenForAccessToken(_ publicToken: String) {
48 | self.communicator.callMyServer(path: "/server/swap_public_token", httpMethod: .post, params: ["public_token": publicToken]) { (result: Result) in
49 | switch result {
50 | case .success(let response):
51 | if response.success {
52 | self.navigationController?.popViewController(animated: true)
53 | } else {
54 | print ("Got a failed success \(response)")
55 | }
56 | case .failure(let error):
57 | print ("Got an error \(error)")
58 | }
59 | }
60 |
61 | }
62 |
63 |
64 | private func fetchLinkToken() {
65 | // Fetch a link token from our server
66 | self.communicator.callMyServer(path: "/server/generate_link_token", httpMethod: .post) { (result: Result) in
67 | switch result {
68 | case .success(let response):
69 | self.linkToken = response.linkToken
70 | self.startLinkButton.isEnabled = true
71 | case .failure(let error):
72 | print(error)
73 | }
74 | }
75 | }
76 |
77 | override func viewDidLoad() {
78 | super.viewDidLoad()
79 | self.startLinkButton.isEnabled = false
80 | fetchLinkToken()
81 | }
82 |
83 |
84 | /*
85 | // MARK: - Navigation
86 |
87 | // In a storyboard-based application, you will often want to do a little preparation before navigation
88 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
89 | // Get the new view controller using segue.destination.
90 | // Pass the selected object to the new view controller.
91 | }
92 | */
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/transactions/finished/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Where'd My Money Go?
12 |
13 |
14 |
15 |
16 |
Where'd My Money Go?
17 |
Let's see how you spent your money!
18 |
19 |
20 |
Create an account!
21 | Account username
22 | Create account!
23 |
24 |
25 |
Sign in!
26 | Or, sign in as one of these users:
27 |
28 | Sign in!
29 |
30 |
31 |
32 |
Welcome back!
33 |
34 |
35 |
36 |
Connect my
37 | bank!
38 |
39 |
40 |
Looking up your transactions
41 |
42 |
43 |
44 | Date
45 | Description
46 | Category
47 | Amount
48 | Account
49 |
50 |
51 |
52 |
53 |
54 |
Debug calls:
55 |
Server
56 | refresh
57 |
Client-side
58 | refresh
59 |
Generate a
60 | webhook
61 |
62 |
63 | Stop using this
64 | bank
65 |
66 |
67 |
68 |
Sign out
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/transactions/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Where'd My Money Go?
12 |
13 |
14 |
15 |
16 |
Where'd My Money Go?
17 |
Let's see how you spent your money!
18 |
19 |
20 |
Create an account!
21 | Account username
22 | Create account!
23 |
24 |
25 |
Sign in!
26 | Or, sign in as one of these users:
27 |
28 | Sign in!
29 |
30 |
31 |
32 |
Welcome back!
33 |
34 |
35 |
36 |
Connect my
37 | bank!
38 |
39 |
40 |
Looking up your transactions
41 |
42 |
43 |
44 | Date
45 | Description
46 | Category
47 | Amount
48 | Account
49 |
50 |
51 |
52 |
53 |
54 |
Debug calls:
55 |
Server
56 | refresh
57 |
Client-side
58 | refresh
59 |
Generate a
60 | webhook
61 |
62 |
63 | Stop using this
64 | bank
65 |
66 |
67 |
68 |
Sign out
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/transactions/start/server/routes/tokens.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const escape = require("escape-html");
3 | const { getLoggedInUserId } = require("../utils");
4 | const db = require("../db");
5 | const { plaidClient } = require("../plaid");
6 | const { syncTransactions } = require("./transactions");
7 |
8 | const router = express.Router();
9 |
10 | const WEBHOOK_URL =
11 | process.env.WEBHOOK_URL || "https://www.example.com/server/receive_webhook";
12 |
13 | /**
14 | * Generates a link token to be used by the client
15 | */
16 | router.post("/generate_link_token", async (req, res, next) => {
17 | try {
18 | const userId = getLoggedInUserId(req);
19 | const userObject = { client_user_id: userId };
20 | const tokenResponse = await plaidClient.linkTokenCreate({
21 | user: userObject,
22 | products: ["identity"],
23 | client_name: "Where'd My Money Go?",
24 | language: "en",
25 | country_codes: ["US"],
26 | webhook: WEBHOOK_URL,
27 | });
28 | res.json(tokenResponse.data);
29 | } catch (error) {
30 | console.log(`Running into an error!`);
31 | next(error);
32 | }
33 | });
34 |
35 | /**
36 | * Exchanges a public token for an access token. Then, fetches a bunch of
37 | * information about that item and stores it in our database
38 | */
39 | router.post("/exchange_public_token", async (req, res, next) => {
40 | try {
41 | const userId = getLoggedInUserId(req);
42 | const publicToken = escape(req.body.publicToken);
43 |
44 | const tokenResponse = await plaidClient.itemPublicTokenExchange({
45 | public_token: publicToken,
46 | });
47 | const tokenData = tokenResponse.data;
48 | await db.addItem(tokenData.item_id, userId, tokenData.access_token);
49 | await populateBankName(tokenData.item_id, tokenData.access_token);
50 | await populateAccountNames(tokenData.access_token);
51 |
52 | /* Placeholder code to show that something works! */
53 | const identityResult = await plaidClient.identityGet({
54 | access_token: tokenData.access_token,
55 | });
56 | console.log(`Here's some info about the account holders:`);
57 | console.dir(identityResult.data, { depth: null, colors: true });
58 |
59 | res.json({ status: "success" });
60 | } catch (error) {
61 | console.log(`Running into an error!`);
62 | next(error);
63 | }
64 | });
65 |
66 | const populateBankName = async (itemId, accessToken) => {
67 | try {
68 | const itemResponse = await plaidClient.itemGet({
69 | access_token: accessToken,
70 | });
71 | const institutionId = itemResponse.data.item.institution_id;
72 | if (institutionId == null) {
73 | return;
74 | }
75 | const institutionResponse = await plaidClient.institutionsGetById({
76 | institution_id: institutionId,
77 | country_codes: ["US"],
78 | });
79 | const institutionName = institutionResponse.data.institution.name;
80 | await db.addBankNameForItem(itemId, institutionName);
81 | } catch (error) {
82 | console.log(`Ran into an error! ${error}`);
83 | }
84 | };
85 |
86 | const populateAccountNames = async (accessToken) => {
87 | try {
88 | const acctsResponse = await plaidClient.accountsGet({
89 | access_token: accessToken,
90 | });
91 | const acctsData = acctsResponse.data;
92 | const itemId = acctsData.item.item_id;
93 | await Promise.all(
94 | acctsData.accounts.map(async (acct) => {
95 | await db.addAccount(acct.account_id, itemId, acct.name);
96 | })
97 | );
98 | } catch (error) {
99 | console.log(`Ran into an error! ${error}`);
100 | }
101 | };
102 |
103 | module.exports = router;
104 |
--------------------------------------------------------------------------------
/transactions/start/server/webhookServer.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const { syncTransactions } = require("./routes/transactions");
5 |
6 | /**
7 | * Our server running on a different port that we'll use for handling webhooks.
8 | * We run this on a separate port so that it's easier to expose just this
9 | * server to the world using a tool like ngrok
10 | */
11 | const WEBHOOK_PORT = process.env.WEBHOOK_PORT || 8001;
12 |
13 | const webhookApp = express();
14 | webhookApp.use(bodyParser.urlencoded({ extended: false }));
15 | webhookApp.use(bodyParser.json());
16 |
17 | const webhookServer = webhookApp.listen(WEBHOOK_PORT, function () {
18 | console.log(
19 | `Webhook receiver is up and running at http://localhost:${WEBHOOK_PORT}/`
20 | );
21 | });
22 |
23 | webhookApp.post("/server/receive_webhook", async (req, res, next) => {
24 | try {
25 | console.log("**INCOMING WEBHOOK**");
26 | console.dir(req.body, { colors: true, depth: null });
27 | const product = req.body.webhook_type;
28 | const code = req.body.webhook_code;
29 |
30 | // TODO (maybe): Verify webhook
31 | switch (product) {
32 | case "ITEM":
33 | handleItemWebhook(code, req.body);
34 | break;
35 | case "TRANSACTIONS":
36 | handleTxnWebhook(code, req.body);
37 | break;
38 | default:
39 | console.log(`Can't handle webhook product ${product}`);
40 | break;
41 | }
42 | res.json({ status: "received" });
43 | } catch (error) {
44 | next(error);
45 | }
46 | });
47 |
48 | function handleTxnWebhook(code, requestBody) {
49 | switch (code) {
50 | // TODO: Handle transaction webhooks here
51 | default:
52 | console.log(`Can't handle webhook code ${code}`);
53 | break;
54 | }
55 | }
56 |
57 | function handleItemWebhook(code, requestBody) {
58 | switch (code) {
59 | case "ERROR":
60 | // The most common reason for receiving this webhook is because your
61 | // user's credentials changed and they should run Link in update mode to fix it.
62 | console.log(
63 | `I received this error: ${requestBody.error.error_message}| should probably ask this user to re-connect to their bank`
64 | );
65 | break;
66 | case "NEW_ACCOUNTS_AVAILABLE":
67 | console.log(
68 | `There are new accounts available at this Financial Institution! (Id: ${requestBody.item_id}) We may want to ask the user to share them with us`
69 | );
70 | break;
71 | case "PENDING_EXPIRATION":
72 | console.log(
73 | `We should tell our user to reconnect their bank with Plaid so there's no disruption to their service`
74 | );
75 | break;
76 | case "USER_PERMISSION_REVOKED":
77 | console.log(
78 | `The user revoked access to this item. We should remove it from our records`
79 | );
80 | break;
81 | case "WEBHOOK_UPDATE_ACKNOWLEDGED":
82 | console.log(`Hooray! You found the right spot!`);
83 | break;
84 | default:
85 | console.log(`Can't handle webhook code ${code}`);
86 | break;
87 | }
88 | }
89 |
90 | /**
91 | * Add in some basic error handling so our server doesn't crash if we run into
92 | * an error.
93 | */
94 | const errorHandler = function (err, req, res, next) {
95 | console.error(`Your error:`);
96 | console.error(err);
97 | if (err.response?.data != null) {
98 | res.status(500).send(err.response.data);
99 | } else {
100 | res.status(500).send({
101 | error_code: "OTHER_ERROR",
102 | error_message: "I got some other message on the server.",
103 | });
104 | }
105 | };
106 | webhookApp.use(errorHandler);
107 |
108 | const getWebhookServer = function () {
109 | return webhookServer;
110 | };
111 |
112 | module.exports = {
113 | getWebhookServer,
114 | };
115 |
--------------------------------------------------------------------------------
/transactions/finished/public/js/client.js:
--------------------------------------------------------------------------------
1 | import { startLink } from "./link.js";
2 | import {
3 | createNewUser,
4 | refreshSignInStatus,
5 | signIn,
6 | signOut,
7 | } from "./signin.js";
8 | import {
9 | callMyServer,
10 | currencyAmount,
11 | humanReadableCategory,
12 | showSelector,
13 | } from "./utils.js";
14 |
15 | export const refreshConnectedBanks = async () => {
16 | const banksMsg = document.querySelector("#banksMsg");
17 | const bankData = await callMyServer("/server/banks/list");
18 | if (bankData == null || bankData.length === 0) {
19 | banksMsg.textContent = "You aren't connected to any banks yet. 🙁";
20 | } else if (bankData.length === 1) {
21 | banksMsg.textContent = `You're connected to ${
22 | bankData[0].bank_name ?? "unknown"
23 | }`;
24 | } else {
25 | // The English language is weird.
26 | banksMsg.textContent =
27 | `You're connected to ` +
28 | bankData
29 | .map((e, idx) => {
30 | return (
31 | (idx == bankData.length - 1 && bankData.length > 1 ? "and " : "") +
32 | (e.bank_name ?? "(Unknown)")
33 | );
34 | })
35 | .join(bankData.length !== 2 ? ", " : " ");
36 | }
37 | document.querySelector("#connectToBank").textContent =
38 | bankData != null && bankData.length > 0
39 | ? "Connect another bank!"
40 | : "Connect a bank!";
41 |
42 | // Fill out our "Remove this bank" drop-down
43 | const bankOptions = bankData.map(
44 | (bank) => `${bank.bank_name} `
45 | );
46 |
47 | const bankSelect = document.querySelector("#deactivateBankSelect");
48 | bankSelect.innerHTML =
49 | `--Pick one-- ` + bankOptions.join("\n");
50 | };
51 |
52 | const showTransactionData = (txnData) => {
53 | const tableRows = txnData.map((txnObj) => {
54 | return `
55 | ${txnObj.date}
56 | ${txnObj.name}
57 | ${humanReadableCategory(txnObj.category)}
58 | ${currencyAmount(
59 | txnObj.amount,
60 | txnObj.currency_code
61 | )}
62 | ${txnObj.bank_name} ${txnObj.account_name}
63 | `;
64 | });
65 | // WARNING: Not really safe without some proper sanitization
66 | document.querySelector("#transactionTable").innerHTML = tableRows.join("\n");
67 | };
68 |
69 | const connectToBank = async () => {
70 | await startLink(() => {
71 | refreshConnectedBanks();
72 | });
73 | };
74 |
75 | export const clientRefresh = async () => {
76 | const txnData = await callMyServer("/server/transactions/list?maxCount=50");
77 | showTransactionData(txnData);
78 | };
79 |
80 | const serverRefresh = async () => {
81 | await callMyServer("/server/transactions/sync", true);
82 | };
83 |
84 | const generateWebhook = async () => {
85 | await callMyServer("/server/debug/generate_webhook", true);
86 | };
87 |
88 | const deactivateBank = async () => {
89 | const itemId = document.querySelector("#deactivateBankSelect").value;
90 | if (itemId != null && itemId !== "") {
91 | await callMyServer("/server/banks/deactivate", true, { itemId: itemId });
92 | await refreshConnectedBanks();
93 | }
94 | };
95 |
96 | // Connect selectors to functions
97 | const selectorsAndFunctions = {
98 | "#createAccount": createNewUser,
99 | "#signIn": signIn,
100 | "#signOut": signOut,
101 | "#connectToBank": connectToBank,
102 | "#serverRefresh": serverRefresh,
103 | "#clientRefresh": clientRefresh,
104 | "#generateWebhook": generateWebhook,
105 | "#deactivateBank": deactivateBank,
106 | };
107 |
108 | Object.entries(selectorsAndFunctions).forEach(([sel, fun]) => {
109 | if (document.querySelector(sel) == null) {
110 | console.warn(`Hmm... couldn't find ${sel}`);
111 | } else {
112 | document.querySelector(sel)?.addEventListener("click", fun);
113 | }
114 | });
115 |
116 | await refreshSignInStatus();
117 |
--------------------------------------------------------------------------------
/transactions/README.md:
--------------------------------------------------------------------------------
1 | # Transactions
2 |
3 | ### Overview
4 |
5 | This is a starter app that is to be used with the Getting Started with Plaid Transactions [YouTube tutorial](https://youtu.be/Pin0-ceDKcI). It implements a very basic personal finance app using `/transactions/sync` to retrieve and update the user's transaction data. This application uses HTML/VanillaJS on the front end, and NodeJS/Express on the backend, alongside a SQLite database.
6 |
7 | ### Running the app
8 |
9 | If you want the most complete instructions for running the app, please follow along with the video tutorial linked above. The following abbreviated instructions are for those of you who really don't want to watch videos.
10 |
11 | #### Clone the repo
12 |
13 | Clone the tutorial resources repo to your machine and cd into the project's start directory:
14 |
15 | ```bash
16 | git clone https://github.com/plaid/tutorial-resources && cd transactions/start
17 | ```
18 |
19 | #### Set up your environment
20 |
21 | This app is designed to be used with Node 16 or higher. For information on installing Node, see [How to install Node.js](https://nodejs.dev/learn/how-to-install-nodejs), and consider using [nvm](https://github.com/nvm-sh/nvm) to easily switch between Node versions.
22 |
23 | #### Install dependencies
24 |
25 | Install the necessary dependencies:
26 |
27 | ```bash
28 | npm install
29 | ```
30 |
31 | #### Equip the app with credentials
32 |
33 | Copy the included **.env.template** to a file called **.env**.
34 |
35 | ```bash
36 | cp .env.template .env
37 | ```
38 |
39 | Fill out the contents of the **.env** file with the [client ID and secret in your Plaid dashboard](https://dashboard.plaid.com/team/keys). Make sure to pick the appropriate secret value for the environment you're using. (We recommend using sandbox to start.)
40 |
41 | #### (Optional) Configure a webhook
42 |
43 | This application also shows you how to make use of webhooks, so your application know when to fetch new transactions. If you wish to use this feature, you'll need to have your webhook server, which is running on port 8001, available to Plaid's server.
44 |
45 | A common way to do this is to use a tool like ngrok. If you have ngrok installed, run the following command:
46 |
47 | ```bash
48 | ngrok http 8001
49 | ```
50 |
51 | And ngrok will open a tunnel from the outside world to port 8001 on your machine. Update the `WEBHOOK_URL` with the domain that ngrok has generated. Your final URL should look something like `https://abde-123-4-567-8.ngrok.io/server/receive_webhook`
52 |
53 | See the [Plaid Webhooks Tutorial](https://www.youtube.com/watch?v=0E0KEAVeDyc) for a full description of webhooks and how they work.
54 |
55 | #### Start the server
56 |
57 | Start the app by running the following command:
58 |
59 | ```bash
60 | npm run watch
61 | ```
62 |
63 | The server will start running on port 8000 and will update whenever you make a change to the server files. To use the app, navigate to `localhost:8000` in your browser.
64 |
65 | #### Sandbox test credentials
66 |
67 | Use `user_transactions_dynamic` as the username, and any non-blank string as the password. You can call `/transactions/refresh` to simulate a transactions update in Sandbox. This will generate new pending transactions, all previously pending transactions will be moved to posted, and the amount of one previous transaction will be incremented by $1.00. All appropriate transaction webhooks will also be fired at this time.
68 |
69 | You can also use the standard `user_good` / `pass_good` credentials, but the transactions data will be less realistic, and you won't be able to simulate updates.
70 |
71 | #### Problems?
72 |
73 | On some occasions (usually if the app fails to start up properly the first time), you will end up in a state where the database file has been created, but none of the tables have been added. This usually manifests as a `SQLITE_ERROR: no such table: users` error. If you receive this error, you can fix it by deleting the `appdata.db` file in your `database` folder and then restarting the server.
74 |
--------------------------------------------------------------------------------
/transactions/finished/server/webhookServer.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const { syncTransactions } = require("./routes/transactions");
5 |
6 | /**
7 | * Our server running on a different port that we'll use for handling webhooks.
8 | * We run this on a separate port so that it's easier to expose just this
9 | * server to the world using a tool like ngrok
10 | */
11 | const WEBHOOK_PORT = process.env.WEBHOOK_PORT || 8001;
12 |
13 | const webhookApp = express();
14 | webhookApp.use(bodyParser.urlencoded({ extended: false }));
15 | webhookApp.use(bodyParser.json());
16 |
17 | const webhookServer = webhookApp.listen(WEBHOOK_PORT, function () {
18 | console.log(
19 | `Webhook receiver is up and running at http://localhost:${WEBHOOK_PORT}/`
20 | );
21 | });
22 |
23 | webhookApp.post("/server/receive_webhook", async (req, res, next) => {
24 | try {
25 | console.log("**INCOMING WEBHOOK**");
26 | console.dir(req.body, { colors: true, depth: null });
27 | const product = req.body.webhook_type;
28 | const code = req.body.webhook_code;
29 |
30 | // TODO (maybe): Verify webhook
31 | switch (product) {
32 | case "ITEM":
33 | handleItemWebhook(code, req.body);
34 | break;
35 | case "TRANSACTIONS":
36 | handleTxnWebhook(code, req.body);
37 | break;
38 | default:
39 | console.log(`Can't handle webhook product ${product}`);
40 | break;
41 | }
42 | res.json({ status: "received" });
43 | } catch (error) {
44 | next(error);
45 | }
46 | });
47 |
48 | function handleTxnWebhook(code, requestBody) {
49 | switch (code) {
50 | case "SYNC_UPDATES_AVAILABLE":
51 | syncTransactions(requestBody.item_id);
52 | break;
53 | // If we're using sync, we don't really need to concern ourselves with the
54 | // other transactions-related webhooks
55 | default:
56 | console.log(`Can't handle webhook code ${code}`);
57 | break;
58 | }
59 | }
60 |
61 | function handleItemWebhook(code, requestBody) {
62 | switch (code) {
63 | case "ERROR":
64 | // The most common reason for receiving this webhook is because your
65 | // user's credentials changed and they should run Link in update mode to fix it.
66 | console.log(
67 | `I received this error: ${requestBody.error.error_message}| should probably ask this user to connect to their bank`
68 | );
69 | break;
70 | case "NEW_ACCOUNTS_AVAILABLE":
71 | console.log(
72 | `There are new accounts available at this Financial Institution! (Id: ${requestBody.item_id}) We may want to ask the user to share them with us`
73 | );
74 | break;
75 | case "PENDING_EXPIRATION":
76 | console.log(
77 | `We should tell our user to reconnect their bank with Plaid so there's no disruption to their service`
78 | );
79 | break;
80 | case "USER_PERMISSION_REVOKED":
81 | console.log(
82 | `The user revoked access to this item. We should remove it from our records`
83 | );
84 | break;
85 | case "WEBHOOK_UPDATE_ACKNOWLEDGED":
86 | console.log(`Hooray! You found the right spot!`);
87 | break;
88 | default:
89 | console.log(`Can't handle webhook code ${code}`);
90 | break;
91 | }
92 | }
93 |
94 | /**
95 | * Add in some basic error handling so our server doesn't crash if we run into
96 | * an error.
97 | */
98 | const errorHandler = function (err, req, res, next) {
99 | console.error(`Your error:`);
100 | console.error(err);
101 | if (err.response?.data != null) {
102 | res.status(500).send(err.response.data);
103 | } else {
104 | res.status(500).send({
105 | error_code: "OTHER_ERROR",
106 | error_message: "I got some other message on the server.",
107 | });
108 | }
109 | };
110 | webhookApp.use(errorHandler);
111 |
112 | const getWebhookServer = function () {
113 | return webhookServer;
114 | };
115 |
116 | module.exports = {
117 | getWebhookServer,
118 | };
119 |
--------------------------------------------------------------------------------
/transactions/start/public/js/client.js:
--------------------------------------------------------------------------------
1 | import { startLink } from "./link.js";
2 | import {
3 | createNewUser,
4 | refreshSignInStatus,
5 | signIn,
6 | signOut,
7 | } from "./signin.js";
8 | import {
9 | callMyServer,
10 | currencyAmount,
11 | humanReadableCategory,
12 | showSelector,
13 | } from "./utils.js";
14 |
15 | export const refreshConnectedBanks = async () => {
16 | const banksMsg = document.querySelector("#banksMsg");
17 | const bankData = await callMyServer("/server/banks/list");
18 | if (bankData == null || bankData.length === 0) {
19 | banksMsg.textContent = "You aren't connected to any banks yet. 🙁";
20 | } else if (bankData.length === 1) {
21 | banksMsg.textContent = `You're connected to ${
22 | bankData[0].bank_name ?? "unknown"
23 | }`;
24 | } else {
25 | // The English language is weird.
26 | banksMsg.textContent =
27 | `You're connected to ` +
28 | bankData
29 | .map((e, idx) => {
30 | return (
31 | (idx == bankData.length - 1 && bankData.length > 1 ? "and " : "") +
32 | (e.bank_name ?? "(Unknown)")
33 | );
34 | })
35 | .join(bankData.length !== 2 ? ", " : " ");
36 | }
37 | document.querySelector("#connectToBank").textContent =
38 | bankData != null && bankData.length > 0
39 | ? "Connect another bank!"
40 | : "Connect a bank!";
41 |
42 | // Fill out our "Remove this bank" drop-down
43 | const bankOptions = bankData.map(
44 | (bank) => `${bank.bank_name} `
45 | );
46 | const bankSelect = document.querySelector("#deactivateBankSelect");
47 | bankSelect.innerHTML =
48 | `--Pick one-- ` + bankOptions.join("\n");
49 | };
50 |
51 | const showTransactionData = (txnData) => {
52 | /*
53 | const tableRows = txnData.map((txnObj) => {
54 | return `
55 | ${txnObj.date}
56 | ${txnObj.name}
57 | ${txnObj.category}
58 | ${txnObj.amount}
59 | ${txnObj.bank_name} ${txnObj.account_name}
60 | `;
61 | });
62 | // WARNING: Not really safe without some proper sanitization
63 | document.querySelector("#transactionTable").innerHTML = tableRows.join("\n");
64 | */
65 | };
66 |
67 | const connectToBank = async () => {
68 | await startLink(() => {
69 | refreshConnectedBanks();
70 | });
71 | };
72 |
73 | export const clientRefresh = async () => {
74 | // Fetch my transactions from the database
75 | /*
76 | const txnData = await callMyServer("/server/transactions/list?maxCount=50");
77 | showTransactionData(txnData);
78 | */
79 | };
80 |
81 | const serverRefresh = async () => {
82 | // Tell my server to fetch new transactions
83 | /*
84 | await callMyServer("/server/transactions/sync", true);
85 | */
86 | };
87 |
88 | const generateWebhook = async () => {
89 | // Tell my server to generate a webhook
90 | /*
91 | await callMyServer("/server/debug/generate_webhook", true);
92 | */
93 | };
94 |
95 | const deactivateBank = async () => {
96 | // Tell my server to remove a bank from my list of active banks
97 | /*
98 | const itemId = document.querySelector("#deactivateBankSelect").value;
99 | if (itemId != null && itemId !== "") {
100 | await callMyServer("/server/banks/deactivate", true, { itemId: itemId });
101 | await refreshConnectedBanks();
102 | }
103 | */
104 | };
105 |
106 | // Connect selectors to functions
107 | const selectorsAndFunctions = {
108 | "#createAccount": createNewUser,
109 | "#signIn": signIn,
110 | "#signOut": signOut,
111 | "#connectToBank": connectToBank,
112 | "#serverRefresh": serverRefresh,
113 | "#clientRefresh": clientRefresh,
114 | "#generateWebhook": generateWebhook,
115 | "#deactivateBank": deactivateBank,
116 | };
117 |
118 | Object.entries(selectorsAndFunctions).forEach(([sel, fun]) => {
119 | if (document.querySelector(sel) == null) {
120 | console.warn(`Hmm... couldn't find ${sel}`);
121 | } else {
122 | document.querySelector(sel)?.addEventListener("click", fun);
123 | }
124 | });
125 |
126 | await refreshSignInStatus();
127 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/start/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const session = require("express-session");
5 | const { Configuration, PlaidApi, PlaidEnvironments } = require("plaid");
6 | const app = express();
7 | const moment = require("moment");
8 |
9 | const APP_PORT = process.env.APP_PORT || 8000;
10 |
11 | // Creates a session key, which we can use to store the user's access token
12 | // (Convenient for demo purposes, bad for a production-level app)
13 | app.use(
14 | session({
15 | secret: process.env.SESSION_SECRET,
16 | saveUninitialized: true,
17 | resave: true,
18 | })
19 | );
20 |
21 | app.use(bodyParser.urlencoded({ extended: false }));
22 | app.use(bodyParser.json());
23 | app.use(express.static("public"));
24 |
25 | const server = app.listen(APP_PORT, function () {
26 | console.log(
27 | `We're up and running. Head on over to http://localhost:${APP_PORT}/`
28 | );
29 | });
30 |
31 | // Configuration for the Plaid client
32 | const config = new Configuration({
33 | basePath: PlaidEnvironments[process.env.PLAID_ENV],
34 | baseOptions: {
35 | headers: {
36 | "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID,
37 | "PLAID-SECRET": process.env.PLAID_SECRET,
38 | "Plaid-Version": "2020-09-14",
39 | },
40 | },
41 | });
42 |
43 | //Instantiate the Plaid client with the configuration
44 | const client = new PlaidApi(config);
45 |
46 | // Checks whether or not the user has an access token for a financial
47 | // institution
48 | app.get("/api/is_user_connected", async (req, res, next) => {
49 | console.log(`Our access token: ${req.session.access_token}`);
50 | return req.session.access_token
51 | ? res.json({ status: true })
52 | : res.json({ status: false });
53 | });
54 |
55 | // Retrieves the name of the bank that we're connected to
56 | app.get("/api/get_bank_name", async (req, res, next) => {
57 | const access_token = req.session.access_token;
58 | const itemResponse = await client.itemGet({ access_token });
59 | const configs = {
60 | institution_id: itemResponse.data.item.institution_id,
61 | country_codes: ["US"],
62 | };
63 | const instResponse = await client.institutionsGetById(configs);
64 | console.log(`Institution Info: ${JSON.stringify(instResponse.data)}`);
65 | const bankName = instResponse.data.institution.name;
66 | res.json({ name: bankName });
67 | });
68 |
69 | //Creates a Link token and returns it
70 | app.get("/api/create_link_token", async (req, res, next) => {
71 | const tokenResponse = await client.linkTokenCreate({
72 | user: { client_user_id: req.sessionID },
73 | client_name: "Vanilla JavaScript Sample",
74 | language: "en",
75 | products: ["transactions"],
76 | country_codes: ["US"],
77 | });
78 | console.log(`Token response: ${JSON.stringify(tokenResponse.data)}`);
79 |
80 | res.json(tokenResponse.data);
81 | });
82 |
83 | // Exchanges the public token from Plaid Link for an access token
84 | app.post("/api/exchange_public_token", async (req, res, next) => {
85 | const exchangeResponse = await client.itemPublicTokenExchange({
86 | public_token: req.body.public_token,
87 | });
88 |
89 | // FOR DEMO PURPOSES ONLY
90 | // You should really store access tokens in a database that's tied to your
91 | // authenticated user id.
92 | console.log(`Exchange response: ${JSON.stringify(exchangeResponse.data)}`);
93 | req.session.access_token = exchangeResponse.data.access_token;
94 | res.json(true);
95 | });
96 |
97 | // Fetches balance data using the Node client library for Plaid
98 | app.get("/api/transactions", async (req, res, next) => {
99 | const access_token = req.session.access_token;
100 | const startDate = moment().subtract(30, "days").format("YYYY-MM-DD");
101 | const endDate = moment().format("YYYY-MM-DD");
102 |
103 | const transactionResponse = await client.transactionsGet({
104 | access_token: access_token,
105 | start_date: startDate,
106 | end_date: endDate,
107 | options: { count: 10 },
108 | });
109 | res.json(transactionResponse.data);
110 | });
111 |
112 | app.listen(process.env.PORT || 8080);
113 |
--------------------------------------------------------------------------------
/webhooks/start/public/js/index.js:
--------------------------------------------------------------------------------
1 | import { initializeLink } from "./connect.js";
2 |
3 | let accountNames = {};
4 |
5 | export const checkConnectedStatus = async function () {
6 | const connectedData = await callMyServer("/server/get_user_info");
7 | if (connectedData.user_status === "connected") {
8 | document.querySelector("#connectedUI").classList.remove("d-none");
9 | document.querySelector("#disconnectedUI").classList.add("d-none");
10 | } else {
11 | document.querySelector("#disconnectedUI").classList.remove("d-none");
12 | document.querySelector("#connectedUI").classList.add("d-none");
13 | initializeLink();
14 | }
15 | };
16 |
17 | const getBalances = async function () {
18 | const balanceData = await callMyServer("/server/balances");
19 | const simplifiedData = balanceData.accounts.map((acct) => {
20 | return {
21 | account: acct.name ?? "(Unknown)",
22 | // Yes, making the incorrect assumption these are always in dollars.
23 | account_mask: acct.mask ?? "",
24 | current_balance:
25 | acct.balances.current != null
26 | ? `$${acct.balances.current.toFixed(2)}`
27 | : "(Unknown)",
28 | };
29 | });
30 | console.table(simplifiedData);
31 | displaySimplifiedData(simplifiedData);
32 | };
33 |
34 | const createAssetReport = async function () {
35 | const reportResponse = await callMyServer("/server/create_asset_report");
36 | displaySimplifiedData([{ asset_report_id: reportResponse.asset_report_id }]);
37 | };
38 |
39 | const getTransactions = async function () {
40 | const transactionData = await callMyServer("/server/transactions");
41 | const simplifiedData = transactionData.transactions.map((t) => {
42 | return {
43 | date: t.date,
44 | vendor: t.name,
45 | category: t.category[0],
46 | amount: `$${t.amount.toFixed(2)}`,
47 | };
48 | });
49 | console.table(simplifiedData);
50 | displaySimplifiedData(simplifiedData);
51 | };
52 |
53 | function displaySimplifiedData(simplifiedData) {
54 | const output = document.querySelector("#output");
55 | output.innerHTML = "";
56 | simplifiedData.forEach((thingToShow) => {
57 | const nextItem = document.createElement("li");
58 | nextItem.textContent = JSON.stringify(thingToShow);
59 | output.appendChild(nextItem);
60 | });
61 | }
62 |
63 | const fireTestWebhook = async function () {
64 | // TODO: FIll this out
65 | };
66 |
67 | const updateWebhook = async function () {
68 | // const newWebhookUrl = document.querySelector("#webhookInput").value;
69 | // if (!newWebhookUrl.startsWith("https://")) {
70 | // alert("How about a real URL here?");
71 | // return false;
72 | // }
73 | // await callMyServer("/server/update_webhook", true, {
74 | // newUrl: newWebhookUrl,
75 | // });
76 | };
77 |
78 | const callMyServer = async function (
79 | endpoint,
80 | isPost = false,
81 | postData = null
82 | ) {
83 | const optionsObj = isPost ? { method: "POST" } : {};
84 | if (isPost && postData !== null) {
85 | optionsObj.headers = { "Content-type": "application/json" };
86 | optionsObj.body = JSON.stringify(postData);
87 | }
88 | const response = await fetch(endpoint, optionsObj);
89 | if (response.status === 500) {
90 | await handleServerError(response);
91 | return;
92 | }
93 | const data = await response.json();
94 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
95 | return data;
96 | };
97 |
98 | const handleServerError = async function (responseObject) {
99 | const error = await responseObject.json();
100 | console.error("I received an error ", error);
101 | };
102 |
103 | document.querySelector("#getBalances").addEventListener("click", getBalances);
104 | document
105 | .querySelector("#getTransactions")
106 | .addEventListener("click", getTransactions);
107 | document
108 | .querySelector("#createAsset")
109 | .addEventListener("click", createAssetReport);
110 | document
111 | .querySelector("#simulateWebhook")
112 | .addEventListener("click", fireTestWebhook);
113 | document
114 | .querySelector("#updateWebhook")
115 | .addEventListener("click", updateWebhook);
116 |
117 | checkConnectedStatus();
118 |
--------------------------------------------------------------------------------
/webhooks/finished/public/js/index.js:
--------------------------------------------------------------------------------
1 | import { initializeLink } from "./connect.js";
2 |
3 | let accountNames = {};
4 |
5 | export const checkConnectedStatus = async function () {
6 | const connectedData = await callMyServer("/server/get_user_info");
7 | if (connectedData.user_status === "connected") {
8 | document.querySelector("#connectedUI").classList.remove("d-none");
9 | document.querySelector("#disconnectedUI").classList.add("d-none");
10 | } else {
11 | document.querySelector("#disconnectedUI").classList.remove("d-none");
12 | document.querySelector("#connectedUI").classList.add("d-none");
13 | initializeLink();
14 | }
15 | };
16 |
17 | const getBalances = async function () {
18 | const balanceData = await callMyServer("/server/balances");
19 | const simplifiedData = balanceData.accounts.map((acct) => {
20 | return {
21 | account: acct.name ?? "(Unknown)",
22 | // Yes, making the incorrect assumption these are always in dollars.
23 | account_mask: acct.mask ?? "",
24 | current_balance:
25 | acct.balances.current != null
26 | ? `$${acct.balances.current.toFixed(2)}`
27 | : "(Unknown)",
28 | };
29 | });
30 | console.table(simplifiedData);
31 | displaySimplifiedData(simplifiedData);
32 | };
33 |
34 | const createAssetReport = async function () {
35 | const reportResponse = await callMyServer("/server/create_asset_report");
36 | displaySimplifiedData([{ asset_report_id: reportResponse.asset_report_id }]);
37 | };
38 |
39 | const getTransactions = async function () {
40 | const transactionData = await callMyServer("/server/transactions");
41 | const simplifiedData = transactionData.transactions.map((t) => {
42 | return {
43 | date: t.date,
44 | vendor: t.name,
45 | category: t.category[0],
46 | amount: `$${t.amount.toFixed(2)}`,
47 | };
48 | });
49 | console.table(simplifiedData);
50 | displaySimplifiedData(simplifiedData);
51 | };
52 |
53 | function displaySimplifiedData(simplifiedData) {
54 | const output = document.querySelector("#output");
55 | output.innerHTML = "";
56 | simplifiedData.forEach((thingToShow) => {
57 | const nextItem = document.createElement("li");
58 | nextItem.textContent = JSON.stringify(thingToShow);
59 | output.appendChild(nextItem);
60 | });
61 | }
62 |
63 | const fireTestWebhook = async function () {
64 | await callMyServer("/server/fire_test_webhook", true);
65 | };
66 |
67 | const updateWebhook = async function () {
68 | const newWebhookUrl = document.querySelector("#webhookInput").value;
69 | if (!newWebhookUrl.startsWith("https://")) {
70 | console.log("How about a real URL here?");
71 | return false;
72 | }
73 | await callMyServer("/server/update_webhook", true, {
74 | newUrl: newWebhookUrl,
75 | });
76 | };
77 |
78 | const callMyServer = async function (
79 | endpoint,
80 | isPost = false,
81 | postData = null
82 | ) {
83 | const optionsObj = isPost ? { method: "POST" } : {};
84 | if (isPost && postData !== null) {
85 | optionsObj.headers = { "Content-type": "application/json" };
86 | optionsObj.body = JSON.stringify(postData);
87 | }
88 | const response = await fetch(endpoint, optionsObj);
89 | if (response.status === 500) {
90 | await handleServerError(response);
91 | return;
92 | }
93 | const data = await response.json();
94 | console.log(`Result from calling ${endpoint}: ${JSON.stringify(data)}`);
95 | return data;
96 | };
97 |
98 | const handleServerError = async function (responseObject) {
99 | const error = await responseObject.json();
100 | console.error("I received an error ", error);
101 | };
102 |
103 | document.querySelector("#getBalances").addEventListener("click", getBalances);
104 | document
105 | .querySelector("#getTransactions")
106 | .addEventListener("click", getTransactions);
107 | document
108 | .querySelector("#createAsset")
109 | .addEventListener("click", createAssetReport);
110 | document
111 | .querySelector("#simulateWebhook")
112 | .addEventListener("click", fireTestWebhook);
113 | document
114 | .querySelector("#updateWebhook")
115 | .addEventListener("click", updateWebhook);
116 |
117 | checkConnectedStatus();
118 |
--------------------------------------------------------------------------------
/vanilla-js-oauth/finished/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const session = require("express-session");
5 | const { Configuration, PlaidApi, PlaidEnvironments } = require("plaid");
6 | const app = express();
7 | const moment = require("moment");
8 |
9 | const APP_PORT = process.env.APP_PORT || 8000;
10 |
11 | // Creates a session key, which we can use to store the user's access token
12 | // (Convenient for demo purposes, bad for a production-level app)
13 | app.use(
14 | session({
15 | secret: process.env.SESSION_SECRET,
16 | saveUninitialized: true,
17 | resave: true,
18 | })
19 | );
20 |
21 | app.use(bodyParser.urlencoded({ extended: false }));
22 | app.use(bodyParser.json());
23 | app.use(express.static("public"));
24 |
25 | const server = app.listen(APP_PORT, function () {
26 | console.log(
27 | `We're up and running. Head on over to http://localhost:${APP_PORT}/`
28 | );
29 | });
30 |
31 | // Configuration for the Plaid client
32 | const config = new Configuration({
33 | basePath: PlaidEnvironments[process.env.PLAID_ENV],
34 | baseOptions: {
35 | headers: {
36 | "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID,
37 | "PLAID-SECRET": process.env.PLAID_SECRET,
38 | "Plaid-Version": "2020-09-14",
39 | },
40 | },
41 | });
42 |
43 | //Instantiate the Plaid client with the configuration
44 | const client = new PlaidApi(config);
45 |
46 | // Checks whether or not the user has an access token for a financial
47 | // institution
48 | app.get("/api/is_user_connected", async (req, res, next) => {
49 | console.log(`Our access token: ${req.session.access_token}`);
50 | return req.session.access_token
51 | ? res.json({ status: true })
52 | : res.json({ status: false });
53 | });
54 |
55 | // Retrieves the name of the bank that we're connected to
56 | app.get("/api/get_bank_name", async (req, res, next) => {
57 | const access_token = req.session.access_token;
58 | const itemResponse = await client.itemGet({ access_token });
59 | const configs = {
60 | institution_id: itemResponse.data.item.institution_id,
61 | country_codes: ["US"],
62 | };
63 | const instResponse = await client.institutionsGetById(configs);
64 | console.log(`Institution Info: ${JSON.stringify(instResponse.data)}`);
65 | const bankName = instResponse.data.institution.name;
66 | res.json({ name: bankName });
67 | });
68 |
69 | //Creates a Link token and returns it
70 | app.get("/api/create_link_token", async (req, res, next) => {
71 | const tokenResponse = await client.linkTokenCreate({
72 | user: { client_user_id: req.sessionID },
73 | client_name: "Vanilla JavaScript Sample",
74 | language: "en",
75 | products: ["transactions"],
76 | country_codes: ["US"],
77 | redirect_uri: "http://localhost:8000/oauth-return.html",
78 | });
79 | console.log(`Token response: ${JSON.stringify(tokenResponse.data)}`);
80 |
81 | res.json(tokenResponse.data);
82 | });
83 |
84 | // Exchanges the public token from Plaid Link for an access token
85 | app.post("/api/exchange_public_token", async (req, res, next) => {
86 | const exchangeResponse = await client.itemPublicTokenExchange({
87 | public_token: req.body.public_token,
88 | });
89 |
90 | // FOR DEMO PURPOSES ONLY
91 | // You should really store access tokens in a database that's tied to your
92 | // authenticated user id.
93 | console.log(`Exchange response: ${JSON.stringify(exchangeResponse.data)}`);
94 | req.session.access_token = exchangeResponse.data.access_token;
95 | res.json(true);
96 | });
97 |
98 | // Fetches balance data using the Node client library for Plaid
99 | app.get("/api/transactions", async (req, res, next) => {
100 | const access_token = req.session.access_token;
101 | const startDate = moment().subtract(30, "days").format("YYYY-MM-DD");
102 | const endDate = moment().format("YYYY-MM-DD");
103 |
104 | const transactionResponse = await client.transactionsGet({
105 | access_token: access_token,
106 | start_date: startDate,
107 | end_date: endDate,
108 | options: { count: 10 },
109 | });
110 | res.json(transactionResponse.data);
111 | });
112 |
113 | app.listen(process.env.PORT || 8080);
114 |
--------------------------------------------------------------------------------
/auth/finished/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Auth Tutorial App
12 |
13 |
14 |
15 |
16 |
Auth Tutorial!
17 |
18 |
19 |
Say, you should connect to your bank to continue.
20 |
Connect my bank
21 |
22 |
23 |
24 |
25 |
More information soon...
26 |
27 |
28 |
29 |
Basic "get my account status" functions
30 |
31 |
32 | Refresh user status
33 |
34 |
35 | Call /accounts/get
36 |
37 |
38 | Refresh auth status
39 |
40 |
41 |
42 |
43 |
Auth functions when connected
44 |
45 |
46 | Get auth numbers
47 |
48 |
49 | Get processor token
50 |
51 |
52 |
53 |
54 |
Debug functions when using Automated Micro-deposits
55 |
56 |
57 | I'm feeling impatient!
58 |
59 |
60 |
61 |
62 |
Debug functions when using Same Day Micro-deposits
63 |
64 |
65 | Verify those deposits!
66 |
67 |
68 |
69 |
Extra credit!
70 |
71 |
72 | Post pending
73 | micro-deposits
74 |
75 |
76 | Check for
77 | posted deposits
78 |
79 |
80 |
81 |
82 |
83 |
Results will go here
85 |
86 |
87 |
88 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/auth/start/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | Auth Tutorial App
12 |
13 |
14 |
15 |
16 |
Auth Tutorial!
17 |
18 |
19 |
Say, you should connect to your bank to continue.
20 |
Connect my bank
21 |
22 |
23 |
24 |
25 |
More information soon...
26 |
27 |
28 |
29 |
Basic "get my account status" functions
30 |
31 |
32 | Refresh user status
33 |
34 |
35 | Call /accounts/get
36 |
37 |
38 | Refresh auth status
39 |
40 |
41 |
42 |
43 |
Auth functions when connected
44 |
45 |
46 | Get auth numbers
47 |
48 |
49 | Get processor token
50 |
51 |
52 |
53 |
54 |
Debug functions when using Automated Micro-deposits
55 |
56 |
57 | I'm feeling impatient!
58 |
59 |
60 |
61 |
62 |
Debug functions when using Same Day Micro-deposits
63 |
64 |
65 | Verify those deposits!
66 |
67 |
68 |
69 |
Extra credit!
70 |
71 |
72 | Post pending
73 | micro-deposits
74 |
75 |
76 | Check for
77 | posted deposits
78 |
79 |
80 |
81 |
82 |
83 |
Results will go here
85 |
86 |
87 |
88 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/ios-swift/start/client/SamplePlaidClient/ServerCommunicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerCommunicator.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/18/23.
6 | //
7 |
8 | import Foundation
9 |
10 | ///
11 | /// Just a helper class that simplifies some of the work involved in calling our server
12 | ///
13 | class ServerCommunicator {
14 |
15 | enum HTTPMethod: String {
16 | case get = "GET"
17 | case post = "POST"
18 | }
19 |
20 | enum Error: LocalizedError {
21 | case invalidUrl(String)
22 | case networkError(String)
23 | case encodingError(String)
24 | case decodingError(String)
25 | case nilData
26 |
27 | var localizedDescription: String {
28 | switch self {
29 | case .invalidUrl(let url): return "Invalid URL: \(url)"
30 | case .networkError(let error): return "Network Error: \(error)"
31 | case .encodingError(let error): return "Encoding Error: \(error)"
32 | case .decodingError(let error): return "Decoding Error: \(error)"
33 | case .nilData: return "Server return null data"
34 | }
35 | }
36 |
37 | var errorDescription: String? {
38 | return localizedDescription
39 | }
40 | }
41 |
42 |
43 | init(baseURL: String = "http://localhost:8000/") {
44 | self.baseURL = baseURL
45 | }
46 |
47 | func callMyServer(
48 | path: String,
49 | httpMethod: HTTPMethod,
50 | params: [String: Any]? = nil,
51 | completion: @escaping (Result) -> Void) {
52 |
53 | let path = path.hasPrefix("/") ? String(path.dropFirst()) : path
54 | let urlString = baseURL + path
55 |
56 | guard let url = URL(string: urlString) else {
57 | completion(.failure(ServerCommunicator.Error.invalidUrl(urlString)))
58 | return
59 | }
60 |
61 | var request = URLRequest(url: url)
62 | request.httpMethod = httpMethod.rawValue
63 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
64 | request.setValue("application/json", forHTTPHeaderField: "Accept")
65 |
66 | switch httpMethod {
67 | case .post where params != nil:
68 | do {
69 | let jsonData = try JSONSerialization.data(withJSONObject: params!, options: [])
70 | request.httpBody = jsonData
71 | } catch {
72 | completion(.failure(.encodingError("\(error)")))
73 | return
74 | }
75 | default:
76 | break
77 | }
78 |
79 | // Create the task
80 | let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
81 |
82 | DispatchQueue.main.async {
83 |
84 | if let error = error {
85 | completion(.failure(.networkError("\(error)")))
86 | return
87 | }
88 |
89 | guard let data = data else {
90 | completion(.failure(.nilData))
91 | return
92 | }
93 | print("Received data from: \(path)")
94 | data.printJson()
95 |
96 | do {
97 | let object = try JSONDecoder().decode(T.self, from: data)
98 | completion(.success(object))
99 |
100 | } catch {
101 | completion(.failure(.decodingError("\(error)")))
102 | }
103 | }
104 | }
105 |
106 | task.resume()
107 | }
108 |
109 | struct DummyDecodable: Decodable { }
110 |
111 | // Convenience method where we don't want to do anything yet with the result, beyond seeing what we get back from the server
112 | func callMyServer(
113 | path: String,
114 | httpMethod: HTTPMethod,
115 | params: [String: Any]? = nil
116 | ) {
117 | callMyServer(path: path, httpMethod: httpMethod, params: params) { (_: Result) in
118 | // Do nothing here
119 | }
120 | }
121 |
122 |
123 | private let baseURL: String
124 | }
125 |
126 | extension Data {
127 |
128 | fileprivate func printJson() {
129 | do {
130 | let json = try JSONSerialization.jsonObject(with: self, options: [])
131 | let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
132 | guard let jsonString = String(data: data, encoding: .utf8) else {
133 | print("Invalid data")
134 | return
135 | }
136 | print(jsonString)
137 | } catch {
138 | print("Error: \(error.localizedDescription)")
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/ios-swift/finished/client/SamplePlaidClient/ServerCommunicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServerCommunicator.swift
3 | // SamplePlaidClient
4 | //
5 | // Created by Todd Kerpelman on 8/18/23.
6 | //
7 |
8 | import Foundation
9 |
10 | ///
11 | /// Just a helper class that simplifies some of the work involved in calling our server
12 | ///
13 | class ServerCommunicator {
14 |
15 | enum HTTPMethod: String {
16 | case get = "GET"
17 | case post = "POST"
18 | }
19 |
20 | enum Error: LocalizedError {
21 | case invalidUrl(String)
22 | case networkError(String)
23 | case encodingError(String)
24 | case decodingError(String)
25 | case nilData
26 |
27 | var localizedDescription: String {
28 | switch self {
29 | case .invalidUrl(let url): return "Invalid URL: \(url)"
30 | case .networkError(let error): return "Network Error: \(error)"
31 | case .encodingError(let error): return "Encoding Error: \(error)"
32 | case .decodingError(let error): return "Decoding Error: \(error)"
33 | case .nilData: return "Server return null data"
34 | }
35 | }
36 |
37 | var errorDescription: String? {
38 | return localizedDescription
39 | }
40 | }
41 |
42 |
43 | init(baseURL: String = "http://localhost:8000/") {
44 | self.baseURL = baseURL
45 | }
46 |
47 | func callMyServer(
48 | path: String,
49 | httpMethod: HTTPMethod,
50 | params: [String: Any]? = nil,
51 | completion: @escaping (Result) -> Void) {
52 |
53 | let path = path.hasPrefix("/") ? String(path.dropFirst()) : path
54 | let urlString = baseURL + path
55 |
56 | guard let url = URL(string: urlString) else {
57 | completion(.failure(ServerCommunicator.Error.invalidUrl(urlString)))
58 | return
59 | }
60 |
61 | var request = URLRequest(url: url)
62 | request.httpMethod = httpMethod.rawValue
63 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
64 | request.setValue("application/json", forHTTPHeaderField: "Accept")
65 |
66 | switch httpMethod {
67 | case .post where params != nil:
68 | do {
69 | let jsonData = try JSONSerialization.data(withJSONObject: params!, options: [])
70 | request.httpBody = jsonData
71 | } catch {
72 | completion(.failure(.encodingError("\(error)")))
73 | return
74 | }
75 | default:
76 | break
77 | }
78 |
79 | // Create the task
80 | let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
81 |
82 | DispatchQueue.main.async {
83 |
84 | if let error = error {
85 | completion(.failure(.networkError("\(error)")))
86 | return
87 | }
88 |
89 | guard let data = data else {
90 | completion(.failure(.nilData))
91 | return
92 | }
93 | print("Received data from: \(path)")
94 | data.printJson()
95 |
96 | do {
97 | let object = try JSONDecoder().decode(T.self, from: data)
98 | completion(.success(object))
99 |
100 | } catch {
101 | completion(.failure(.decodingError("\(error)")))
102 | }
103 | }
104 | }
105 |
106 | task.resume()
107 | }
108 |
109 | struct DummyDecodable: Decodable { }
110 |
111 | // Convenience method where we don't want to do anything yet with the result, beyond seeing what we get back from the server
112 | func callMyServer(
113 | path: String,
114 | httpMethod: HTTPMethod,
115 | params: [String: Any]? = nil
116 | ) {
117 | callMyServer(path: path, httpMethod: httpMethod, params: params) { (_: Result) in
118 | // Do nothing here
119 | }
120 | }
121 |
122 |
123 | private let baseURL: String
124 | }
125 |
126 | extension Data {
127 |
128 | fileprivate func printJson() {
129 | do {
130 | let json = try JSONSerialization.jsonObject(with: self, options: [])
131 | let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
132 | guard let jsonString = String(data: data, encoding: .utf8) else {
133 | print("Invalid data")
134 | return
135 | }
136 | print(jsonString)
137 | } catch {
138 | print("Error: \(error.localizedDescription)")
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # VSCode settings (usually altered for screencasting purposes)
107 | .vscode/
108 |
109 | # User settings
110 | user_data.json
111 | .prettierrc
112 |
113 | # User files that could contain sensitive information
114 | **/user_files/user_data*.json
115 |
116 |
117 |
118 |
119 | # We don't want to save our database in git if we're storing
120 | # access tokens in there
121 | appdata.db
122 |
123 |
124 | # Xcode
125 | #
126 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
127 |
128 | ## User settings
129 | xcuserdata/
130 |
131 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
132 | *.xcscmblueprint
133 | *.xccheckout
134 |
135 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
136 | build/
137 | DerivedData/
138 | *.moved-aside
139 | *.pbxuser
140 | !default.pbxuser
141 | *.mode1v3
142 | !default.mode1v3
143 | *.mode2v3
144 | !default.mode2v3
145 | *.perspectivev3
146 | !default.perspectivev3
147 |
148 | ## Obj-C/Swift specific
149 | *.hmap
150 |
151 | ## App packaging
152 | *.ipa
153 | *.dSYM.zip
154 | *.dSYM
155 |
156 | ## Playgrounds
157 | timeline.xctimeline
158 | playground.xcworkspace
159 |
160 | # Swift Package Manager
161 | #
162 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
163 | # Packages/
164 | # Package.pins
165 | # Package.resolved
166 | # *.xcodeproj
167 | #
168 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
169 | # hence it is not needed unless you have added a package configuration file to your project
170 | # .swiftpm
171 |
172 | .build/
173 |
174 | # CocoaPods
175 | #
176 | # We recommend against adding the Pods directory to your .gitignore. However
177 | # you should judge for yourself, the pros and cons are mentioned at:
178 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
179 | #
180 | # Pods/
181 | #
182 | # Add this line if you want to avoid checking in source code from the Xcode workspace
183 | # *.xcworkspace
184 |
185 | # Carthage
186 | #
187 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
188 | # Carthage/Checkouts
189 |
190 | Carthage/Build/
191 |
192 | # Accio dependency management
193 | Dependencies/
194 | .accio/
195 |
196 | # fastlane
197 | #
198 | # It is recommended to not store the screenshots in the git repo.
199 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
200 | # For more information about the recommended setup visit:
201 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
202 |
203 | fastlane/report.xml
204 | fastlane/Preview.html
205 | fastlane/screenshots/**/*.png
206 | fastlane/test_output
207 |
208 | # Code Injection
209 | #
210 | # After new code Injection tools there's a generated folder /iOSInjectionProject
211 | # https://github.com/johnno1962/injectionforxcode
212 |
213 | iOSInjectionProject/
214 |
215 |
--------------------------------------------------------------------------------