diff --git a/.env.example b/.env.example index bd020eb7..8e48b421 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,32 @@ -# Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/account/keys +# Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/team/keys PLAID_CLIENT_ID= PLAID_SECRET= + # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment # Use 'development' to test with real credentials while developing # Use 'production' to go live with real users PLAID_ENV=sandbox + # PLAID_PRODUCTS is a comma-separated list of products to use when # initializing Link, e.g. PLAID_PRODUCTS=auth,transactions. # see https://plaid.com/docs/api/tokens/#link-token-create-request-products for a complete list. # Only institutions that support ALL listed products will be displayed in Link. # If you don't see the institution you want in Link, remove any products you aren't using. -# Important: When moving to Production, make sure to update this list with only the products +# Important: +# When moving to Production, make sure to update this list with only the products # you plan to use. Otherwise, you may be billed for unneeded products. -# NOTE: Income_verification has to be used seperately from all other products due to the specific -# flow. +# NOTE: +# - 'income_verification' has to be used separately from all other products due to the specific flow. +# - 'payment_initiation' has to be used separately from all other products due to the specific flow. PLAID_PRODUCTS=auth,transactions + # PLAID_COUNTRY_CODES is a comma-separated list of countries to use when # initializing Link, e.g. PLAID_COUNTRY_CODES=US,CA. -# see https://plaid.com/docs/api/tokens/#link-token-create-request-country-codes for a complete list +# Institutions from all listed countries will be shown. If Link is launched with multiple country codes, +# only products that you are enabled for in all countries will be used by Link. +# See https://plaid.com/docs/api/tokens/#link-token-create-request-country-codes for a complete list PLAID_COUNTRY_CODES=US,CA + # Only required for OAuth: # For sandbox, set PLAID_REDIRECT_URI to 'http://localhost:3000/' # The OAuth redirect flow requires an endpoint on the developer's website diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4eaa98df..2984193e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,7 @@ import Context from "./Context"; import styles from "./App.module.scss"; const App = () => { - const { linkSuccess, isItemAccess, dispatch } = useContext(Context); + const { linkSuccess, isItemAccess, isPaymentInitiation, dispatch } = useContext(Context); const getInfo = useCallback(async () => { const response = await fetch("/api/info", { method: "POST" }); @@ -24,14 +24,16 @@ const App = () => { type: "SET_STATE", state: { products: data.products, + isPaymentInitiation: paymentInitiation, }, }); return { paymentInitiation }; }, [dispatch]); const generateToken = useCallback( - async (paymentInitiation) => { - const path = paymentInitiation + async (isPaymentInitiation) => { + // Link tokens for 'payment_initiation' use a different creation flow in your backend. + const path = isPaymentInitiation ? "/api/create_link_token_for_payment" : "/api/create_link_token"; const response = await fetch(path, { @@ -55,7 +57,8 @@ const App = () => { } dispatch({ type: "SET_STATE", state: { linkToken: data.link_token } }); } - localStorage.setItem("link_token", data.link_token); //to use later for Oauth + // Save the link_token to be used later in the Oauth flow. + localStorage.setItem("link_token", data.link_token); }, [dispatch] ); @@ -83,10 +86,17 @@ const App = () => {
- {linkSuccess && isItemAccess && ( + {linkSuccess && ( <> - - + {isPaymentInitiation && ( + + )} + {isItemAccess && ( + <> + + + + )} )}
diff --git a/frontend/src/Components/Headers/index.tsx b/frontend/src/Components/Headers/index.tsx index 1d87f9a7..ea8419c4 100644 --- a/frontend/src/Components/Headers/index.tsx +++ b/frontend/src/Components/Headers/index.tsx @@ -17,6 +17,7 @@ const Header = () => { isItemAccess, backend, linkTokenError, + isPaymentInitiation, } = useContext(Context); return ( @@ -90,40 +91,64 @@ const Header = () => { ) : ( <> - {isItemAccess ? ( + {isPaymentInitiation ? ( + <>

- Congrats! By linking an account, you have created an{" "} - - Item - - . -

- ) : ( -

- - Unable to create an item. Please check your backend server + Congrats! Your payment is now confirmed. +

+ + You can see information of all your payments in the{' '} + + Payments Dashboard + + .

- )} -
-

- item_id - {itemId} -

- -

- access_token - {accessToken} -

-
- {isItemAccess && (

- Now that you have an access_token, you can make all of the - following requests: + Now that the 'payment_id' stored in your server, you can use it to access the payment information:

+ + ) : /* If not using the payment_initiation product, show the item_id and access_token information */ ( + <> + {isItemAccess ? ( +

+ Congrats! By linking an account, you have created an{" "} + + Item + + . +

+ ) : ( +

+ + Unable to create an item. Please check your backend server + +

+ )} +
+

+ item_id + {itemId} +

+ +

+ access_token + {accessToken} +

+
+ {isItemAccess && ( +

+ Now that you have an access_token, you can make all of the + following requests: +

+ )} + )} )} diff --git a/frontend/src/Components/Link/index.tsx b/frontend/src/Components/Link/index.tsx index e4d6f00b..f39473b5 100644 --- a/frontend/src/Components/Link/index.tsx +++ b/frontend/src/Components/Link/index.tsx @@ -3,14 +3,15 @@ import { usePlaidLink } from "react-plaid-link"; import Button from "plaid-threads/Button"; import Context from "../../Context"; +import {Products} from "plaid"; const Link = () => { - const { linkToken, dispatch } = useContext(Context); + const { linkToken, isPaymentInitiation, dispatch } = useContext(Context); const onSuccess = React.useCallback( (public_token: string) => { - // send public_token to server - const setToken = async () => { + // If the access_token is needed, send public_token to server + const exchangePublicTokenForAccessToken = async () => { const response = await fetch("/api/set_access_token", { method: "POST", headers: { @@ -39,7 +40,14 @@ const Link = () => { }, }); }; - setToken(); + + // 'payment_initiation' products do not require the public_token to be exchanged for an access_token. + if (isPaymentInitiation){ + dispatch({ type: "SET_STATE", state: { isItemAccess: false } }); + } else { + exchangePublicTokenForAccessToken(); + } + dispatch({ type: "SET_STATE", state: { linkSuccess: true } }); window.history.pushState("", "", "/"); }, diff --git a/frontend/src/Components/ProductTypes/Products.tsx b/frontend/src/Components/ProductTypes/Products.tsx index bec5d920..37c7201b 100644 --- a/frontend/src/Components/ProductTypes/Products.tsx +++ b/frontend/src/Components/ProductTypes/Products.tsx @@ -42,31 +42,37 @@ const Products = () => { transformData={transformPaymentData} /> )} - - - + {products.includes("auth") && ( + + )} + {products.includes("transactions") && ( + + )} + {products.includes("identity") && ( + + )} {products.includes("assets") && ( { transformData={transformAssetsData} /> )} - + transformData={transformBalanceData} + /> + )} {products.includes("investments") && ( <> plaidProd LinkTokenCreateRequestPaymentInitiation paymentInitiation = new LinkTokenCreateRequestPaymentInitiation() .paymentId(paymentId); + // This should correspond to a unique id for the current user. + // Typically, this will be a user ID number from your application. + // Personally identifiable information, such as an email address or phone number, should not be used here. String clientUserId = Long.toString((new Date()).getTime()); LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser() .clientUserId(clientUserId); @@ -89,7 +92,9 @@ public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProd LinkTokenCreateRequest request = new LinkTokenCreateRequest() .user(user) .clientName("Quickstart Client") + // The 'payment_initiation' product has to be the only element in the 'products' list. .products(Arrays.asList(Products.PAYMENT_INITIATION)) + // Institutions from all listed countries will be shown. .countryCodes(Arrays.asList(CountryCode.GB)) .language("en") .redirectUri(this.redirectUri) diff --git a/node/index.js b/node/index.js index 093138d5..ed9f0d1a 100644 --- a/node/index.js +++ b/node/index.js @@ -2,8 +2,9 @@ // read env vars from .env file require('dotenv').config(); -const { Configuration, PlaidApi, PlaidEnvironments } = require('plaid'); +const { Configuration, PlaidApi, Products, PlaidEnvironments} = require('plaid'); const util = require('util'); +const { v4: uuidv4 } = require('uuid'); const express = require('express'); const bodyParser = require('body-parser'); const moment = require('moment'); @@ -17,7 +18,7 @@ const PLAID_ENV = process.env.PLAID_ENV || 'sandbox'; // PLAID_PRODUCTS is a comma-separated list of products to use when initializing // Link. Note that this list must contain 'assets' in order for the app to be // able to create and retrieve asset reports. -const PLAID_PRODUCTS = (process.env.PLAID_PRODUCTS || 'transactions').split( +const PLAID_PRODUCTS = (process.env.PLAID_PRODUCTS || Products.Transactions).split( ',', ); @@ -45,9 +46,9 @@ const PLAID_ANDROID_PACKAGE_NAME = process.env.PLAID_ANDROID_PACKAGE_NAME || ''; let ACCESS_TOKEN = null; let PUBLIC_TOKEN = null; let ITEM_ID = null; -// The payment_id is only relevant for the UK Payment Initiation product. +// The payment_id is only relevant for the UK/EU Payment Initiation product. // We store the payment_id in memory - in production, store it in a secure -// persistent data store +// persistent data store along with the Payment metadata, such as userId . let PAYMENT_ID = null; // The transfer_id is only relevant for Transfer ACH product. // We store the transfer_id in memory - in production, store it in a secure @@ -117,8 +118,11 @@ app.post('/api/create_link_token', function (request, response, next) { .catch(next); }); -// Create a link token with configs which we can then use to initialize Plaid Link client-side. -// See https://plaid.com/docs/#payment-initiation-create-link-token-request +// Create a link token with configs which we can then use to initialize Plaid Link client-side +// for a 'payment-initiation' flow. +// See: +// - https://plaid.com/docs/payment-initiation/ +// - https://plaid.com/docs/#payment-initiation-create-link-token-request app.post( '/api/create_link_token_for_payment', function (request, response, next) { @@ -149,16 +153,24 @@ app.post( }); prettyPrintResponse(createPaymentResponse); const paymentId = createPaymentResponse.data.payment_id; + + // We store the payment_id in memory for demo purposes - in production, store it in a secure + // persistent data store along with the Payment metadata, such as userId. PAYMENT_ID = paymentId; + const configs = { + client_name: 'Plaid Quickstart', user: { // This should correspond to a unique id for the current user. - client_user_id: 'user-id', + // Typically, this will be a user ID number from your application. + // Personally identifiable information, such as an email address or phone number, should not be used here. + client_user_id: uuidv4(), }, - client_name: 'Plaid Quickstart', - products: PLAID_PRODUCTS, + // Institutions from all listed countries will be shown. country_codes: PLAID_COUNTRY_CODES, language: 'en', + // The 'payment_initiation' product has to be the only element in the 'products' list. + products: [Products.PaymentInitiation], payment_initiation: { payment_id: paymentId, }, @@ -187,10 +199,11 @@ app.post('/api/set_access_token', function (request, response, next) { prettyPrintResponse(tokenResponse); ACCESS_TOKEN = tokenResponse.data.access_token; ITEM_ID = tokenResponse.data.item_id; - if (PLAID_PRODUCTS.includes('transfer')) { + if (PLAID_PRODUCTS.includes(Products.Transfer)) { TRANSFER_ID = await authorizeAndCreateTransfer(ACCESS_TOKEN); } response.json({ + // the 'access_token' is a private token, DO NOT pass this token to the frontend in your production environment access_token: ACCESS_TOKEN, item_id: ITEM_ID, error: null, @@ -345,7 +358,7 @@ app.get('/api/item', function (request, response, next) { // Also pull information about the institution const configs = { institution_id: itemResponse.data.item.institution_id, - country_codes: ['US'], + country_codes: PLAID_COUNTRY_CODES, }; const instResponse = await client.institutionsGetById(configs); prettyPrintResponse(itemResponse); @@ -443,7 +456,7 @@ app.get('/api/transfer', function (request, response, next) { .catch(next); }); -// This functionality is only relevant for the UK Payment Initiation product. +// This functionality is only relevant for the UK/EU Payment Initiation product. // Retrieve Payment for a specified Payment ID app.get('/api/payment', function (request, response, next) { Promise.resolve() diff --git a/node/package-lock.json b/node/package-lock.json index 13113e0a..775a9dee 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -1211,6 +1211,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/node/package.json b/node/package.json index fd7906fd..585db933 100644 --- a/node/package.json +++ b/node/package.json @@ -19,6 +19,7 @@ "express": "4.16.x", "moment": "2.22.x", "nodemon": "^2.0.5", - "plaid": "^9.9.0" + "plaid": "^9.9.0", + "uuid": "^9.0.0" } } diff --git a/python/server.py b/python/server.py index 51187e72..dd20bbe3 100644 --- a/python/server.py +++ b/python/server.py @@ -175,13 +175,22 @@ def create_link_token_for_payment(): request ) pretty_print_response(response.to_dict()) + + # We store the payment_id in memory for demo purposes - in production, store it in a secure + # persistent data store along with the Payment metadata, such as userId. payment_id = response['payment_id'] + linkRequest = LinkTokenCreateRequest( + # The 'payment_initiation' product has to be the only element in the 'products' list. products=[Products('payment_initiation')], client_name='Plaid Test', + # Institutions from all listed countries will be shown. country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)), language='en', user=LinkTokenCreateRequestUser( + # This should correspond to a unique id for the current user. + # Typically, this will be a user ID number from your application. + # Personally identifiable information, such as an email address or phone number, should not be used here. client_user_id=str(time.time()) ), payment_initiation=LinkTokenCreateRequestPaymentInitiation( diff --git a/ruby/app.rb b/ruby/app.rb index 1f714653..741be985 100644 --- a/ruby/app.rb +++ b/ruby/app.rb @@ -398,10 +398,13 @@ def nil_if_empty_envvar(field) end end -# This functionality is only relevant for the UK Payment Initiation product. +# This functionality is only relevant for the UK/EU Payment Initiation product. # Sets the payment token in memory on the server side. We generate a new # payment token so that the developer is not required to supply one. # This makes the quickstart easier to use. +# See: +# - https://plaid.com/docs/payment-initiation/ +# - https://plaid.com/docs/#payment-initiation-create-link-token-request post '/api/create_link_token_for_payment' do begin payment_initiation_recipient_create_request = Plaid::PaymentInitiationRecipientCreateRequest.new( @@ -451,12 +454,24 @@ def nil_if_empty_envvar(field) link_token_create_request = Plaid::LinkTokenCreateRequest.new( { - user: { client_user_id: 'user-id' }, - client_name: 'Plaid Quickstart', - products: ENV['PLAID_PRODUCTS'].split(','), + client_name: 'Plaid Quickstart', + user: { + # This should correspond to a unique id for the current user. + # Typically, this will be a user ID number from your application. + # Personally identifiable information, such as an email address or phone number, should not be used here. + client_user_id: 'user-id' + }, + + # Institutions from all listed countries will be shown. country_codes: ENV['PLAID_COUNTRY_CODES'].split(','), language: 'en', - payment_initiation: { payment_id: payment_id }, + + # The 'payment_initiation' product has to be the only element in the 'products' list. + products: ['payment_initiation'], + + payment_initiation: { + payment_id: payment_id + }, redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI') } )