├── 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 | 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 | 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) => `` 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) => `` 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 | 20 |
21 |
22 |

You're connected!

23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 |

39 | 43 |

44 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 | 56 |
57 | 58 |
59 |
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 | 20 |
21 |
22 |

You're connected!

23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 |

39 | 43 |

44 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 | 56 |
57 | 58 |
59 |
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 | 22 | 23 |
24 |
25 |

Sign in!

26 | Or, sign in as one of these users: 27 | 28 | 29 |
30 |
31 |
32 |

Welcome back!

33 | 34 |
35 |

36 | 38 |
39 |
40 |

Looking up your transactions

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
DateDescriptionCategoryAmountAccount
53 |
54 |

Debug calls:

55 | 57 | 59 | 61 |
62 | 63 | 65 |
66 |
67 |
68 | 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 | 22 | 23 |
24 |
25 |

Sign in!

26 | Or, sign in as one of these users: 27 | 28 | 29 |
30 |
31 |
32 |

Welcome back!

33 | 34 |
35 |

36 | 38 |
39 |
40 |

Looking up your transactions

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
DateDescriptionCategoryAmountAccount
53 |
54 |

Debug calls:

55 | 57 | 59 | 61 |
62 | 63 | 65 |
66 |
67 |
68 | 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) => `` 45 | ); 46 | 47 | const bankSelect = document.querySelector("#deactivateBankSelect"); 48 | bankSelect.innerHTML = 49 | `` + 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) => `` 45 | ); 46 | const bankSelect = document.querySelector("#deactivateBankSelect"); 47 | bankSelect.innerHTML = 48 | `` + 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 | 21 |
      22 |
      23 |
      24 |
      25 |

      More information soon...

      26 |

      27 |
      28 |
      29 |

      Basic "get my account status" functions

      30 |
      31 |
      32 | 33 |
      34 |
      35 | 36 |
      37 |
      38 | 39 |
      40 |
      41 |
      42 |
      43 |

      Auth functions when connected

      44 |
      45 |
      46 | 47 |
      48 |
      49 | 50 |
      51 |
      52 |
      53 |
      54 |

      Debug functions when using Automated Micro-deposits

      55 |
      56 |
      57 | 58 |
      59 |
      60 |
      61 |
      62 |

      Debug functions when using Same Day Micro-deposits

      63 |
      64 |
      65 | 66 |
      67 |
      68 |
      69 |

      Extra credit!

      70 |
      71 |
      72 | 74 |
      75 |
      76 | 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 | 21 |
      22 |
      23 |
      24 |
      25 |

      More information soon...

      26 |

      27 |
      28 |
      29 |

      Basic "get my account status" functions

      30 |
      31 |
      32 | 33 |
      34 |
      35 | 36 |
      37 |
      38 | 39 |
      40 |
      41 |
      42 |
      43 |

      Auth functions when connected

      44 |
      45 |
      46 | 47 |
      48 |
      49 | 50 |
      51 |
      52 |
      53 |
      54 |

      Debug functions when using Automated Micro-deposits

      55 |
      56 |
      57 | 58 |
      59 |
      60 |
      61 |
      62 |

      Debug functions when using Same Day Micro-deposits

      63 |
      64 |
      65 | 66 |
      67 |
      68 |
      69 |

      Extra credit!

      70 |
      71 |
      72 | 74 |
      75 |
      76 | 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 | --------------------------------------------------------------------------------