From f66b26350385b0fd712a796ed18d4fc185261560 Mon Sep 17 00:00:00 2001 From: Alejandro Garcia Marra Date: Tue, 25 Oct 2022 13:24:37 +0200 Subject: [PATCH 1/3] [EUWECRY-1084] Refactor QuickStart app for Payment Initiation --- .env.example | 16 +++- frontend/src/App.tsx | 30 ++++-- frontend/src/Components/Headers/index.tsx | 91 ++++++++++++------- frontend/src/Components/Link/index.tsx | 17 ++-- .../src/Components/ProductTypes/Products.tsx | 74 ++++++++------- frontend/src/Context/index.tsx | 2 + go/server.go | 27 ++++-- .../LinkTokenWithPaymentResource.java | 5 + node/index.js | 37 +++++--- node/package-lock.json | 5 + node/package.json | 3 +- python/server.py | 9 ++ ruby/app.rb | 25 ++++- 13 files changed, 231 insertions(+), 110 deletions(-) diff --git a/.env.example b/.env.example index bd020eb7..5d2d03f5 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 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..7167c1fb 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' require a few extra steps to be executed by 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 use later in the Oauth flow. + localStorage.setItem("link_token", data.link_token); }, [dispatch] ); @@ -83,11 +86,20 @@ 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..a8cd6ef7 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 ? ( -

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

- ) : ( -

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

- )} -
-

- item_id - {itemId} -

+ {!isPaymentInitiation ? ( + <> + {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: -

+

+ access_token + {accessToken} +

+
+ {isItemAccess && ( +

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

+ )} + + ) : ( + <> +

+ Congrats! Your payment is now confirmed. +

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

+

+ Now that the 'payment_id' stored in your server, you can use it to access the payment information: +

+ )} )} diff --git a/frontend/src/Components/Link/index.tsx b/frontend/src/Components/Link/index.tsx index e4d6f00b..6bb009ce 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) => { + (public_token: string, metadata: any) => { // send public_token to server - const setToken = async () => { + const exchangePublicToken = async () => { const response = await fetch("/api/set_access_token", { method: "POST", headers: { @@ -39,7 +40,13 @@ const Link = () => { }, }); }; - setToken(); + // 'payment_initiation' products do not require the public_token to be exchanged for an access_token. + if (!isPaymentInitiation){ + exchangePublicToken(); + } else { + dispatch({ type: "SET_STATE", state: { isItemAccess: false } }); + } + dispatch({ type: "SET_STATE", state: { linkSuccess: true } }); window.history.pushState("", "", "/"); }, @@ -53,8 +60,6 @@ const Link = () => { }; if (window.location.href.includes("?oauth_state_id=")) { - // TODO: figure out how to delete this ts-ignore - // @ts-ignore config.receivedRedirectUri = window.location.href; isOauth = true; } 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') } ) From c5a0a7bb9f93646b46cfdeb19ca3a39155d90410 Mon Sep 17 00:00:00 2001 From: Alejandro Garcia Marra Date: Tue, 25 Oct 2022 13:34:59 +0200 Subject: [PATCH 2/3] [EUWECRY-1084] Refactor QuickStart app for Payment Initiation --- frontend/src/App.tsx | 28 +++--- frontend/src/Components/Headers/index.tsx | 100 +++++++++++----------- frontend/src/Components/Link/index.tsx | 15 ++-- 3 files changed, 72 insertions(+), 71 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7167c1fb..2984193e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,7 @@ const App = () => { const generateToken = useCallback( async (isPaymentInitiation) => { - // Link tokens for 'payment_initiation' require a few extra steps to be executed by your backend. + // 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"; @@ -57,7 +57,7 @@ const App = () => { } dispatch({ type: "SET_STATE", state: { linkToken: data.link_token } }); } - // Save the link_token to be use later in the Oauth flow. + // Save the link_token to be used later in the Oauth flow. localStorage.setItem("link_token", data.link_token); }, [dispatch] @@ -87,19 +87,17 @@ const App = () => {
{linkSuccess && ( - <> - {isPaymentInitiation && ( - <> - - - )} - {isItemAccess && ( - <> - - - - )} - + <> + {isPaymentInitiation && ( + + )} + {isItemAccess && ( + <> + + + + )} + )}
diff --git a/frontend/src/Components/Headers/index.tsx b/frontend/src/Components/Headers/index.tsx index a8cd6ef7..ea8419c4 100644 --- a/frontend/src/Components/Headers/index.tsx +++ b/frontend/src/Components/Headers/index.tsx @@ -91,64 +91,64 @@ const Header = () => { ) : ( <> - {!isPaymentInitiation ? ( + {isPaymentInitiation ? ( <> - {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: -

- )} - - ) : ( +

+ Congrats! Your payment is now confirmed. +

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

+

+ 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 */ ( <> -

- Congrats! Your payment is now confirmed. -

- - You can see information of all your payments in the{' '} + {isItemAccess ? ( +

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

-

- Now that the 'payment_id' stored in your server, you can use it to access the payment information: + + ) : ( +

+ + 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 6bb009ce..f39473b5 100644 --- a/frontend/src/Components/Link/index.tsx +++ b/frontend/src/Components/Link/index.tsx @@ -9,9 +9,9 @@ const Link = () => { const { linkToken, isPaymentInitiation, dispatch } = useContext(Context); const onSuccess = React.useCallback( - (public_token: string, metadata: any) => { - // send public_token to server - const exchangePublicToken = async () => { + (public_token: string) => { + // 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: { @@ -40,11 +40,12 @@ const Link = () => { }, }); }; + // 'payment_initiation' products do not require the public_token to be exchanged for an access_token. - if (!isPaymentInitiation){ - exchangePublicToken(); - } else { + if (isPaymentInitiation){ dispatch({ type: "SET_STATE", state: { isItemAccess: false } }); + } else { + exchangePublicTokenForAccessToken(); } dispatch({ type: "SET_STATE", state: { linkSuccess: true } }); @@ -60,6 +61,8 @@ const Link = () => { }; if (window.location.href.includes("?oauth_state_id=")) { + // TODO: figure out how to delete this ts-ignore + // @ts-ignore config.receivedRedirectUri = window.location.href; isOauth = true; } From 5c8c0b96adfd8c18c9c51c0a8c20f23ac702881f Mon Sep 17 00:00:00 2001 From: Alejandro Garcia Marra Date: Wed, 26 Oct 2022 11:50:04 +0200 Subject: [PATCH 3/3] Update dashboard keys URL --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 5d2d03f5..8e48b421 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# 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=