ynap 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ynap/cli'
4
+
5
+ Ynap::CLI.start
@@ -0,0 +1,29 @@
1
+ :plaid:
2
+ :client_id: 1234567890a
3
+ :public_key: 1234567890a
4
+ :secret: 1234567890a
5
+ :env: development
6
+ :country_codes: FR
7
+ :redirect_uri: https://{subdomain}.ngrok.io/oauth-response.html
8
+ :web_port: 8000
9
+ :ynab:
10
+ :token: your-token
11
+ :budget_id: your-id-in-ynab-url
12
+ :regex:
13
+ - ^(?:PRLV|VIR|E-VIR)(?:\sSEPA)?\s(?<name>[^,]*)(?:,.*)?$ # VIR/PRLV SEPA PAYEE NAME(, something)
14
+ - ^CARTE \d{1,2}\/\d{1,2}\/\d{1,2}\s(?<name>[\w\s]*)(?:\sCB\*\d*)?$ # CARTE 16/10/20 PAYEE NAME CB*6825
15
+ - ^CB\s\d*\s(?<name>.{11}).*$ # CB 123123 PAYEE NAME 11CITY LE 1111 - PAYEE NAME IS 11 chars
16
+ :banks:
17
+ - :id: bansky
18
+ :name: Bansky
19
+ :plaid_access_token: access-development-123456789
20
+ :accounts:
21
+ - :plaid_id: account-plaid-id
22
+ :ynab_id: account-ynab-id
23
+ :start_date: "2020-10-23"
24
+ - :id: piggy
25
+ :name: Piggy Bank
26
+ :plaid_access_token: access-development-123456789
27
+ :accounts:
28
+ - :plaid_id: account-plaid-id
29
+ :ynab_id: account-ynab-id
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ynap"
4
+ require "ynap/cli"
5
+
6
+ Ynap::CLI.start(ARGV)
@@ -0,0 +1,950 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Plaid Quickstart Example</title>
6
+ <link rel="stylesheet" href="https://threads.plaid.com/threads.css">
7
+
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <style>
10
+ /*
11
+
12
+ Base
13
+
14
+ */
15
+
16
+ body {
17
+ background: #ffffff;
18
+ }
19
+
20
+ .main {
21
+ max-width: 960px;
22
+ margin-right: auto;
23
+ margin-left: auto;
24
+ padding: 80px 40px;
25
+ }
26
+
27
+
28
+ /*
29
+
30
+ Content at the top of the view
31
+
32
+ */
33
+ .loading-indicator {
34
+ -webkit-animation: rot 1200ms infinite cubic-bezier(0.23, 1.2, 0.32, 1);
35
+ animation: rot 1200ms infinite cubic-bezier(0.23, 1.2, 0.32, 1);
36
+ border-bottom: 2px solid #e3e3e3;
37
+ border-left: 2px solid #e3e3e3;
38
+ border-radius: 100%;
39
+ border-right: 2px solid #e3e3e3;
40
+ border-top: 2px solid #7e7e7e;
41
+ font-size: 100%;
42
+ font: inherit;
43
+ height: 45px;
44
+ left: calc(50% - (45px / 2));
45
+ margin: 0;
46
+ padding: 0;
47
+ position: absolute;
48
+ top: 80%;
49
+ vertical-align: baseline;
50
+ width: 45px;
51
+ box-sizing: border-box
52
+ }
53
+ @keyframes rot {
54
+ from {
55
+ -webkit-transform: rotate(0deg);
56
+ transform: rotate(0deg);
57
+ }
58
+ to {
59
+ -webkit-transform: rotate(359deg);
60
+ transform: rotate(359deg);
61
+ }
62
+ }
63
+ @-webkit-keyframes rot {
64
+ from {
65
+ -webkit-transform: rotate(0deg);
66
+ transform: rotate(0deg);
67
+ }
68
+ to {
69
+ -webkit-transform: rotate(359deg);
70
+ transform: rotate(359deg);
71
+ }
72
+ }
73
+
74
+ .everpresent-content {
75
+ border-bottom-width: 1px;
76
+ border-bottom-color: #E3E9EF;
77
+ border-bottom-style: solid;
78
+ margin-bottom: 16px;
79
+ padding-bottom: 16px;
80
+ }
81
+
82
+ .everpresent-content__heading {
83
+ margin-bottom: 16px;
84
+ }
85
+
86
+ .everpresent-content__subheading {
87
+ font-size: 24px;
88
+ font-weight: 300;
89
+ color: #527084;
90
+ line-height: 32px;
91
+ margin-bottom: 0;
92
+ }
93
+
94
+ /*
95
+
96
+ Item overview
97
+
98
+ */
99
+
100
+ .item-overview {
101
+ padding-bottom: 16px;
102
+ }
103
+
104
+ .item-overview__column {
105
+ width: 50%;
106
+ float: left;
107
+ }
108
+
109
+ .item-overview__column:nth-child(1) {
110
+ padding-right: 8px;
111
+ }
112
+
113
+ .item-overview__column:nth-child(2) {
114
+ padding-left: 8px;
115
+ }
116
+
117
+ .item-overview__heading {
118
+ font-weight: bold;
119
+ text-transform: uppercase;
120
+ font-size: 12px;
121
+ margin-bottom: 0;
122
+ }
123
+
124
+ .item-overview__id {
125
+ text-overflow: ellipsis;
126
+ overflow-x: hidden;
127
+ }
128
+
129
+ /*
130
+
131
+ One off tweaks to the layout for tables
132
+
133
+ */
134
+
135
+ .response-row {
136
+ }
137
+
138
+ .response-row--is-identity td {
139
+ width: 25%;
140
+ display: inline-block;
141
+ overflow-wrap: break-word;
142
+ vertical-align: top;
143
+ }
144
+
145
+ /*
146
+
147
+ Display content inside of box
148
+
149
+ */
150
+
151
+ .box {
152
+ border-radius: 4px;
153
+ border-width: 1px;
154
+ border-color: #E3E9EF;
155
+ border-style: solid;
156
+ border-radius: 4px;
157
+ margin-bottom: 24px;
158
+ overflow: hidden;
159
+ box-shadow: 0 1px 2px rgba(3,49,86,0.2);
160
+ }
161
+
162
+ .box__heading {
163
+ padding: 16px 24px;
164
+ border-bottom-width: 1px;
165
+ border-bottom-color: #E3E9EF;
166
+ border-bottom-style: solid;
167
+ background-color: #FAFCFD;
168
+ margin-bottom: 0;
169
+ }
170
+
171
+
172
+ /*
173
+
174
+ Item row
175
+
176
+ */
177
+ .item-data-row {
178
+ border-bottom-width: 1px;
179
+ border-bottom-color: #E3E9EF;
180
+ border-bottom-style: solid;
181
+ padding-top: 24px;
182
+ padding-bottom: 24px;
183
+ padding-left: 24px;
184
+ padding-right: 24px;
185
+ }
186
+
187
+ .item-data-row:after {
188
+ clear: both;
189
+ content: '';
190
+ display: table;
191
+ }
192
+
193
+ .item-data-row:last-child {
194
+ margin-bottom: 0;
195
+ border-bottom: 0;
196
+ }
197
+
198
+ .item-data-row__request-type {
199
+ background-color
200
+ font-size: 12px;
201
+ color: #02B1F8;
202
+ letter-spacing: 0;
203
+ background: #D9F3FE;
204
+ font-weight: 600;
205
+ text-transform: uppercase;
206
+ border-radius: 4px;
207
+ height: 32px;
208
+ line-height: 32px;
209
+ text-align: center;
210
+ font-size: 14px;
211
+ }
212
+
213
+ .item-data-row__endpoint {
214
+ font-family: "monaco", courier;
215
+ font-size: 14px;
216
+ line-height: 32px;
217
+ display: inline-block;
218
+ }
219
+
220
+ .item-data-row__nicename {
221
+ font-size: 16px;
222
+ line-height: 32px;
223
+ display: inline-block;
224
+ font-weight: 500;
225
+ color: #033156;
226
+ }
227
+
228
+
229
+ .item-data-row__left {
230
+ width: 13%;
231
+ float: left;
232
+ }
233
+
234
+ .item-data-row__center {
235
+ width: 62%;
236
+ float: left;
237
+ padding-left: 16px;
238
+ }
239
+
240
+ .item-data-row__right {
241
+ width: 25%;
242
+ float: left;
243
+ }
244
+
245
+ /*
246
+
247
+ Hide things
248
+
249
+ */
250
+
251
+ #app {
252
+ display: none;
253
+ }
254
+
255
+ #steps {
256
+ display: none;
257
+ }
258
+
259
+ /*
260
+
261
+ Errors
262
+
263
+ */
264
+
265
+ .alert {
266
+ padding: 15px;
267
+ margin-top: 20px;
268
+ margin-bottom: 20px;
269
+ border: 1px solid transparent;
270
+ border-radius: 4px;
271
+ }
272
+
273
+ .alert-danger {
274
+ color: #a94442;
275
+ background-color: #f2dede;
276
+ border-color: #ebccd1;
277
+ }
278
+
279
+ .alert-danger a {
280
+ color: #a94442;
281
+ font-weight: bold;
282
+ }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <main class="main">
287
+ <div class="grid">
288
+ <div class="grid__column grid__column--is-twelve-columns">
289
+ <div id="banner" class="everpresent-content">
290
+ <h1 class="everpresent-content__heading">Plaid Quickstart</h1>
291
+ <p id="intro" class="everpresent-content__subheading">
292
+ An example application that outlines an end-to-end integration with Plaid
293
+ </p>
294
+ <p id="steps" class="everpresent-content__subheading">
295
+ Success! You just created an Item by linking your account.
296
+ </p>
297
+ </div>
298
+
299
+ <div id="container" class="initial-view">
300
+ <p class="initial-view__description">
301
+ Click the button below to open a list of Institutions. After you select one, you’ll be guided through an authentication process. Upon completion, a public_token will be passed back to the server and exchanged for access_token.
302
+ </p>
303
+
304
+ <button id="link-btn" class="button button--is-primary" disabled>Connect with Plaid</button>
305
+ <div class="loading-indicator"></div>
306
+
307
+ </div>
308
+
309
+ <div id="app" class="connected-view">
310
+ <div class="item-overview">
311
+ <div class="item-overview__column">
312
+ <h3 class="item-overview__heading">item_id</h3>
313
+ <p class="item-overview__id" id="item_id">san.asjsansakjsakjasjksajkas</p>
314
+ </div>
315
+ <div class="item-overview__column">
316
+ <h3 class="item-overview__heading">access_token</h3>
317
+ <p class="item-overview__id" id="access_token">••••••••hsakjsl</p>
318
+ </div>
319
+ <div style="clear: both"></div>
320
+ </div>
321
+
322
+ <p>Now that you have an access_token you can make all of the following API requests:</p>
323
+
324
+ <div class="box">
325
+ <h3 class="box__heading">Products</h3>
326
+
327
+ <!-- Auth -->
328
+ <div class="item-data-row">
329
+ <div class="item-data-row__left">
330
+ <div class="item-data-row__request-type">post</div>
331
+ </div>
332
+ <div class="item-data-row__center">
333
+ <div class="item-data-row__nicename">Auth</div>
334
+ <div class="item-data-row__endpoint">/auth/get</div>
335
+ <div class="item-data-row__description">Retrieve account and routing numbers for checking and savings accounts.</div>
336
+ </div>
337
+ <div class="item-data-row__right">
338
+ <button id="get-auth-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
339
+ </div>
340
+ <div class="item-data-row__response">
341
+ <table><tbody id="get-auth-data"></tbody></table>
342
+ </div>
343
+ </div>
344
+
345
+ <!--Transactions -->
346
+ <div class="item-data-row">
347
+ <div class="item-data-row__left">
348
+ <div class="item-data-row__request-type">post</div>
349
+ </div>
350
+ <div class="item-data-row__center">
351
+ <div class="item-data-row__nicename">Transactions</div>
352
+ <div class="item-data-row__endpoint">/transactions/get</div>
353
+ <div class="item-data-row__description">Retrieve transactions for credit and depository accounts.</div>
354
+ </div>
355
+ <div class="item-data-row__right">
356
+ <button id="get-transactions-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
357
+ </div>
358
+ <div class="item-data-row__response">
359
+ <table><tbody id="get-transactions-data"></tbody></table>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- Identity -->
364
+ <div class="item-data-row">
365
+ <div class="item-data-row__left">
366
+ <div class="item-data-row__request-type">post</div>
367
+ </div>
368
+ <div class="item-data-row__center">
369
+ <div class="item-data-row__nicename">Identity</div>
370
+ <div class="item-data-row__endpoint">/identity/get</div>
371
+ <div class="item-data-row__description">Retrieve Identity information on file with the bank. Reduce fraud by comparing user-submitted data to validate identity.</div>
372
+ </div>
373
+ <div class="item-data-row__right">
374
+ <button id="get-identity-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
375
+ </div>
376
+ <div class="item-data-row__response">
377
+ <table><tbody id="get-identity-data"></tbody></table>
378
+ </div>
379
+ </div>
380
+
381
+ <!-- Balance -->
382
+ <div class="item-data-row">
383
+ <div class="item-data-row__left">
384
+ <div class="item-data-row__request-type">post</div>
385
+ </div>
386
+ <div class="item-data-row__center">
387
+ <div class="item-data-row__nicename">Balance</div>
388
+ <div class="item-data-row__endpoint">/accounts/balance/get</div>
389
+ <div class="item-data-row__description">Check balances in real time to prevent non-sufficient funds fees.</div>
390
+ </div>
391
+ <div class="item-data-row__right">
392
+ <button id="get-balance-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
393
+ </div>
394
+ <div class="item-data-row__response">
395
+ <table><tbody id="get-balance-data"></tbody></table>
396
+ </div>
397
+ </div>
398
+
399
+ <!-- Assets (hidden unless 'assets' is included in the product list) -->
400
+ <div id='assets' class="item-data-row" style='display:none;'>
401
+ <div class="item-data-row__left">
402
+ <div class="item-data-row__request-type">post</div>
403
+ </div>
404
+ <div class="item-data-row__center">
405
+ <div class="item-data-row__nicename">Assets</div>
406
+ <div class="item-data-row__endpoint">/asset_report/*</div>
407
+ <div class="item-data-row__description">Create a point-in-time snapshot of a user's assets.</div>
408
+ </div>
409
+
410
+ <div class="item-data-row__right">
411
+ <button id="get-assets-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
412
+
413
+ <a id="download-assets-pdf-btn" class="button button--is-small button--is-primary button--is-full-width" style="display: none; margin: 10px 0px;";>Download as PDF</a>
414
+ </div>
415
+
416
+ <div class="item-data-row__response">
417
+ <table><tbody id="get-assets-data"></tbody></table>
418
+ </div>
419
+ </div>
420
+
421
+ <!-- Holdings -->
422
+ <div class="item-data-row">
423
+ <div class="item-data-row__left">
424
+ <div class="item-data-row__request-type">post</div>
425
+ </div>
426
+ <div class="item-data-row__center">
427
+ <div class="item-data-row__nicename">Holdings</div>
428
+ <div class="item-data-row__endpoint">/investments/holdings/get</div>
429
+ <div class="item-data-row__description">Retrieve investment holdings on file with the bank, brokerage, or investment institution. Analyze over-exposure to market segments.</div>
430
+ </div>
431
+ <div class="item-data-row__right">
432
+ <button id="get-holdings-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
433
+ </div>
434
+ <div class="item-data-row__response">
435
+ <table><tbody id="get-holdings-data"></tbody></table>
436
+ </div>
437
+ </div>
438
+
439
+ <!-- Investment Transactions -->
440
+ <div class="item-data-row">
441
+ <div class="item-data-row__left">
442
+ <div class="item-data-row__request-type">post</div>
443
+ </div>
444
+ <div class="item-data-row__center">
445
+ <div class="item-data-row__nicename">Investment Transactions</div>
446
+ <div class="item-data-row__endpoint">/investments/transactions/get</div>
447
+ <div class="item-data-row__description">Retrieve investment transactions to analyze cash flow and trading performance.</div>
448
+ </div>
449
+ <div class="item-data-row__right">
450
+ <button id="get-investment-transactions-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
451
+ </div>
452
+ <div class="item-data-row__response">
453
+ <table><tbody id="get-investment-transactions-data"></tbody></table>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- UK Payment Initiation (hidden unless 'payment_initiation' is included in the product list) -->
458
+ <div class="payment_initiation item-data-row" style='display:none;'>
459
+ <div class="item-data-row__left">
460
+ <div class="item-data-row__request-type">post</div>
461
+ </div>
462
+ <div class="item-data-row__center">
463
+ <div class="item-data-row__nicename">Payment Initiation</div>
464
+ <div class="item-data-row__endpoint">/payment_initiation/payment/get</div>
465
+ <div class="item-data-row__description">Retrieve payment initiation status for the payment you just initiated.</div>
466
+ </div>
467
+
468
+ <div class="item-data-row__right">
469
+ <button id="get-payment-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
470
+ </div>
471
+
472
+ <div class="item-data-row__response">
473
+ <table><tbody id="get-payment-data"></tbody></table>
474
+ </div>
475
+ </div>
476
+
477
+ </div>
478
+
479
+ <div class="box">
480
+ <h3 class="box__heading">Item management</h3>
481
+
482
+ <div class="item-data-row">
483
+ <div class="item-data-row__left">
484
+ <div class="item-data-row__request-type">post</div>
485
+ </div>
486
+ <div class="item-data-row__center">
487
+ <div class="item-data-row__endpoint">/item/get</div>
488
+ <div class="item-data-row__description">Retrieve information about an Item, like the institution, billed products, available products, and webhook information.</div>
489
+ </div>
490
+
491
+ <div class="item-data-row__right">
492
+ <button id="get-item-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
493
+ </div>
494
+
495
+ <div class="item-data-row__response">
496
+ <table><tbody id="get-item-data"></tbody></table>
497
+ </div>
498
+ </div>
499
+
500
+ <div class="item-data-row">
501
+ <div class="item-data-row__left">
502
+ <div class="item-data-row__request-type">post</div>
503
+ </div>
504
+ <div class="item-data-row__center">
505
+ <div class="item-data-row__endpoint">/accounts/get</div>
506
+ <div class="item-data-row__description">Retrieve high-level information about all accounts associated with an Item.</div>
507
+ </div>
508
+
509
+ <div class="item-data-row__right">
510
+ <button id="get-accounts-btn" class="button button--is-small button--is-default button--is-full-width">Send request</button>
511
+ </div>
512
+
513
+ <div class="item-data-row__response">
514
+ <table><tbody id="get-accounts-data"></tbody></table>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </main>
522
+
523
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
524
+ <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
525
+ <script>
526
+ function render_page($, page_info) {
527
+ // Handles redirect from the oauth response page for the oauth flow.
528
+ if (document.referrer != null && document.referrer.includes('http://localhost:8000/oauth-response.html') && page_info.item_id != null && page_info.item_id.length > 0) {
529
+ $('#container').fadeOut('fast', function() {
530
+ $('#item_id').text(page_info.item_id);
531
+ $('#access_token').text(page_info.access_token);
532
+ $('#intro').hide();
533
+ $('#app, #steps').fadeIn('slow');
534
+ });
535
+ }
536
+
537
+ var products = page_info.products;
538
+ if (products.includes('assets')) {
539
+ $('#assets').show();
540
+ }
541
+ // This functionality is only relevant for the UK Payment Initiation product.
542
+ if (products.includes('payment_initiation')) {
543
+ $('.payment_initiation').show();
544
+ $.post('/api/create_link_token_for_payment', {}, function(data) {
545
+ if (data.error != null) {
546
+ $('.loading-indicator').hide();
547
+ displayError($('#container'), data.error);
548
+ return;
549
+ }
550
+ localStorage.setItem('link_token', data.link_token);
551
+ // In the case of payment_initiation product, we need to wait for a
552
+ // link token configured with payment initiation to be generated before
553
+ // the Link handler can be initialized.
554
+ handler = Plaid.create({
555
+ token: data.link_token,
556
+ onSuccess: function(public_token) {
557
+ // This public token exchange step is not relevant for the
558
+ // payment_initiation product and should be skipped.
559
+ $.post('/api/set_access_token', {
560
+ public_token: public_token
561
+ }, function(data) {
562
+ $('#container').fadeOut('fast', function() {
563
+ $('#item_id').text(data.item_id);
564
+ $('#access_token').text(data.access_token);
565
+ $('#intro').hide();
566
+ $('#app, #steps').fadeIn('slow');
567
+ });
568
+ });
569
+ },
570
+ });
571
+ $('#link-btn').attr('disabled', false);
572
+ $('.loading-indicator').hide();
573
+ });
574
+ } else {
575
+ var handler = null;
576
+ $.post('/api/create_link_token', {}, function(data){
577
+ if (data.error != null) {
578
+ $('.loading-indicator').hide();
579
+ displayError($('#container'), data.error);
580
+ return;
581
+ }
582
+ localStorage.setItem('link_token', data.link_token);
583
+ handler = Plaid.create({
584
+ token: data.link_token,
585
+ onSuccess: function(public_token) {
586
+ $.post('/api/set_access_token', {
587
+ public_token: public_token
588
+ }, function(data) {
589
+ $('#container').fadeOut('fast', function() {
590
+ $('#item_id').text(data.item_id);
591
+ $('#access_token').text(data.access_token);
592
+ $('#intro').hide();
593
+ $('#app, #steps').fadeIn('slow');
594
+ });
595
+ });
596
+ },
597
+ });
598
+ $('#link-btn').attr('disabled', false);
599
+ $('.loading-indicator').hide();
600
+ });
601
+ }
602
+
603
+ var accessToken = qs('access_token');
604
+ if (accessToken != null && accessToken !== '') {
605
+ $.post('/api/set_access_token', {
606
+ access_token: accessToken
607
+ }, function(data) {
608
+ $('#container').fadeOut('fast', function() {
609
+ $('#item_id').text(data.item_id);
610
+ $('#access_token').text(accessToken);
611
+ $('#intro').hide();
612
+ $('#app, #steps').fadeIn('slow');
613
+ });
614
+ });
615
+ }
616
+
617
+ $('#link-btn').on('click', function(e) {
618
+ if (handler != null) {
619
+ handler.open();
620
+ }
621
+ });
622
+
623
+ $('#get-accounts-btn').on('click', function(e) {
624
+ $.get('/api/accounts', function(data) {
625
+
626
+ $('#get-accounts-data').slideUp(function() {
627
+ if (data.error != null) {
628
+ displayError(this, data.error);
629
+ return;
630
+ }
631
+ var html = '<tr><td><strong>Name</strong></td><td><strong>Balances</strong></td><td><strong>Subtype</strong></td><td><strong>Mask</strong></td></tr>';
632
+ data.accounts.forEach(function(account, idx) {
633
+ html += '<tr>';
634
+ html += '<td>' + account.name + '</td>';
635
+ html += '<td>$' + (account.balances.available != null ? account.balances.available : account.balances.current) + '</td>';
636
+ html += '<td>' + account.subtype + '</td>';
637
+ html += '<td>' + account.mask + '</td>';
638
+ html += '</tr>';
639
+ });
640
+
641
+ $(this).html(html).slideDown();
642
+ });
643
+ });
644
+ });
645
+
646
+ $('#get-auth-btn').on('click', function(e) {
647
+ $.get('/api/auth', function(data) {
648
+ $('#get-auth-data').slideUp(function() {
649
+ if (data.error != null) {
650
+ displayError(this, data.error);
651
+ return;
652
+ }
653
+ var isAch = data.numbers.ach.length > 0;
654
+ var routingLabel = isAch ? 'Routing #' : 'Institution and Branch #';
655
+
656
+ var html = '<tr><td><strong>Name</strong></td><td><strong>Balance</strong></td><td><strong>Account #</strong></td><td><strong>'+ routingLabel +'</strong></td></tr>';
657
+ if (isAch) {
658
+ data.numbers.ach.forEach(function(achNumbers, idx) {
659
+ // Find the account associated with this set of account and routing numbers
660
+ var account = data.accounts.filter(function(a) {
661
+ return a.account_id === achNumbers.account_id;
662
+ })[0];
663
+ html += '<tr>';
664
+ html += '<td>' + account.name + '</td>';
665
+ html += '<td>$' + (account.balances.available != null ? account.balances.available : account.balances.current) + '</td>';
666
+ html += '<td>' + achNumbers.account + '</td>';
667
+ html += '<td>' + achNumbers.routing + '</td>';
668
+ html += '</tr>';
669
+ });
670
+ } else {
671
+ data.numbers.eft.forEach(function(eftNumber, idx) {
672
+ // Find the account associated with this set of account and routing numbers
673
+ var account = data.accounts.filter(function(a) {
674
+ return a.account_id === eftNumber.account_id;
675
+ })[0];
676
+ html += '<tr>';
677
+ html += '<td>' + account.name + '</td>';
678
+ html += '<td>$' + (account.balances.available != null ? account.balances.available : account.balances.current) + '</td>';
679
+ html += '<td>' + eftNumber.account + '</td>';
680
+ html += '<td>' + eftNumber.institution + ' ' + eftNumber.branch + '</td>';
681
+ html += '</tr>';
682
+ });
683
+ }
684
+ $(this).html(html).slideDown();
685
+ });
686
+ });
687
+ });
688
+
689
+ $('#get-identity-btn').on('click', function(e) {
690
+ $.get('/api/identity', function(data) {
691
+ $('#get-identity-data').slideUp(function() {
692
+ if (data.error != null) {
693
+ displayError(this, data.error);
694
+ return;
695
+ }
696
+ var html = '<tr class="response-row response-row--is-identity"><td><strong>Names</strong></td><td><strong>Emails</strong></td><td><strong>Phone numbers</strong></td><td><strong>Addresses</strong></td></tr><tr class="response-row response-row--is-identity">';
697
+ var identityData = data.identity[0];
698
+ var html = '<tr class="response-row response-row--is-identity"><td><strong>Names</strong></td><td><strong>Emails</strong></td><td><strong>Phone numbers</strong></td><td><strong>Addresses</strong></td></tr><tr class="response-row response-row--is-identity">';
699
+
700
+ identityData.owners.forEach(function(owner, idx) {
701
+ html += '<td>';
702
+ owner.names.forEach(function(name, idx) {
703
+ html += name + '<br />';
704
+ });
705
+ html += '</td><td>';
706
+ owner.emails.forEach(function(email, idx) {
707
+ html += email.data + '<br />';
708
+ });
709
+ html += '</td><td>';
710
+ owner.phone_numbers.forEach(function(number, idx) {
711
+ html += number.data + '<br />';
712
+ });
713
+ html += '</td><td>';
714
+ owner.addresses.forEach(function(address, idx) {
715
+ html += address.data.street + '<br />';
716
+ html += address.data.city + ', ' + address.data.region + ' ' + address.data.postal_code + '<br />';
717
+ });
718
+ html += '</td>';
719
+ html += '</tr>';
720
+
721
+ });
722
+
723
+ $(this).html(html).slideDown();
724
+ });
725
+ });
726
+ });
727
+
728
+ $('#get-item-btn').on('click', function(e) {
729
+ $.get('/api/item', function(data) {
730
+ $('#get-item-data').slideUp(function() {
731
+ if (data.error != null) {
732
+ displayError(this, data.error);
733
+ return;
734
+ }
735
+ var html = '';
736
+ html += '<tr><td>Institution name</td><td>' + data.institution.name + '</td></tr>';
737
+ html += '<tr><td>Billed products</td><td>' + data.item.billed_products.join(', ') + '</td></tr>';
738
+ html += '<tr><td>Available products</td><td>' + data.item.available_products.join(', ') + '</td></tr>';
739
+
740
+ $(this).html(html).slideDown();
741
+ });
742
+ });
743
+ });
744
+
745
+ $('#get-transactions-btn').on('click', function(e) {
746
+ $.get('/api/transactions', function(data) {
747
+ if (data.error != null && data.error.error_code != null) {
748
+ // Format the error
749
+ var errorHtml = '<div class="inner"><p>' +
750
+ '<strong>' + data.error.error_code + ':</strong> ' +
751
+ (data.error.display_message == null ? data.error.error_message : data.error.display_message) + '</p></div>';
752
+
753
+ if (data.error.error_code === 'PRODUCT_NOT_READY') {
754
+ // Add additional context for `PRODUCT_NOT_READY` errors
755
+ errorHtml += '<div class="inner"><p>Note: The PRODUCT_NOT_READY ' +
756
+ 'error is returned when a request to retrieve Transaction data ' +
757
+ 'is made before Plaid finishes the <a href="https://plaid.com/' +
758
+ 'docs/quickstart/#transaction-data-with-webhooks">initial ' +
759
+ 'transaction pull.</a></p></div>';
760
+ }
761
+ // Render the error
762
+ $('#get-transactions-data').slideUp(function() {
763
+ $(this).slideUp(function() {
764
+ $(this).html(errorHtml).slideDown();
765
+ });
766
+ });
767
+ return;
768
+ }
769
+ $('#get-transactions-data').slideUp(function () {
770
+ var html = '<tr><td><strong>Name</strong></td><td><strong>Amount</strong></td><td><strong>Date</strong></td></tr>';
771
+ data.transactions.forEach(function (txn, idx) {
772
+ html += '<tr>';
773
+ html += '<td>' + txn.name + '</td>';
774
+ html += '<td>$' + txn.amount + '</td>';
775
+ html += '<td>' + txn.date + '</td>';
776
+ html += '</tr>';
777
+ });
778
+
779
+ $(this).slideUp(function () {
780
+ $(this).html(html).slideDown();
781
+ });
782
+ });
783
+ });
784
+ });
785
+
786
+ $('#get-balance-btn').on('click', function(e) {
787
+ $.get('/api/balance', function(data) {
788
+ $('#get-balance-data').slideUp(function() {
789
+ if (data.error != null) {
790
+ displayError(this, data.error);
791
+ return;
792
+ }
793
+ var html = '<tr><td><strong>Name</strong></td><td><strong>Balance</strong></td><td><strong>Subtype</strong></td><td><strong>Mask</strong></td></tr>';
794
+ data.accounts.forEach(function(account, idx) {
795
+ html += '<tr>';
796
+ html += '<td>' + account.name + '</td>';
797
+ html += '<td>$' + (account.balances.available != null ? account.balances.available : account.balances.current) + '</td>'
798
+ html += '<td>' + account.subtype + '</td>';
799
+ html += '<td>' + account.mask + '</td>';
800
+ html += '</tr>';
801
+ });
802
+
803
+ $(this).html(html).slideDown();
804
+ });
805
+ });
806
+ });
807
+
808
+ $('#get-holdings-btn').on('click', function(e) {
809
+ $.get('/api/holdings', function(data) {
810
+ $('#get-holdings-data').slideUp(function() {
811
+ if (data.error != null) {
812
+ displayError(this, data.error);
813
+ return;
814
+ }
815
+ // Organize by Account
816
+ var holdingsData = data.holdings.holdings.sort(function(a, b) {
817
+ if (a.account_id > b.account_id) return 1;
818
+ return -1;
819
+ });
820
+ var html = '<tr class="response-row response-row--is-holdings"></tr><td><strong>Account Mask</strong></td><td><strong>Name</strong></td><td><strong>Quantity</strong></td><td><strong>Close Price</strong></td><td><strong>Value</strong></td><tr class="response-row response-row--is-holding">';
821
+ holdingsData.forEach(function(h, idx) {
822
+ const account = data.holdings.accounts.filter(a => a.account_id === h.account_id)[0];
823
+ const security = data.holdings.securities.filter(s => s.security_id === h.security_id)[0];
824
+ if (account == null) {
825
+ displayError(this, {
826
+ code: 500,
827
+ type: 'Internal',
828
+ display_message: 'Holding lacks a account',
829
+ });
830
+ }
831
+ if (security == null) {
832
+ displayError(this, {
833
+ code: 500,
834
+ type: 'Internal',
835
+ display_message: 'Holding lacks a security',
836
+ });
837
+ }
838
+ html += '<tr>';
839
+ html += '<td>' + account.mask + '</td>';
840
+ html += '<td>' + security.name + '</td>';
841
+ html += '<td>' + h.quantity + '</td>';
842
+ html += '<td>$' + security.close_price + '</td>';
843
+ html += '<td>$' + h.quantity * security.close_price + '</td>';
844
+ html += '</tr>';
845
+ });
846
+ $(this).html(html).slideDown();
847
+ });
848
+ });
849
+ });
850
+
851
+ $('#get-investment-transactions-btn').on('click', function(e) {
852
+ $.get('/api/investment_transactions', function(data) {
853
+ $('#get-investment-transactions-data').slideUp(function() {
854
+ if (data.error != null) {
855
+ displayError(this, data.error);
856
+ return;
857
+ }
858
+ investmentTransactionData = data.investment_transactions.investment_transactions;
859
+ var html = '<tr class="response-row response-row--is-investment-transactions"></tr><td><strong>Name</strong></td><td><strong>Amount</strong></td><td><strong>Date</strong></td><tr class="response-row response-row--is-investment-transaction">';
860
+ investmentTransactionData.forEach(function(invTxn, idx) {
861
+ html += '<tr>';
862
+ html += '<td>' + invTxn.name + '</td>';
863
+ html += '<td>$' + invTxn.amount + '</td>';
864
+ html += '<td>' + invTxn.date + '</td>';
865
+ html += '</tr>';
866
+ });
867
+ $(this).html(html).slideDown();
868
+ });
869
+ });
870
+ });
871
+
872
+ $('#get-assets-btn').on('click', function(e) {
873
+ $.get('/api/assets', function(data) {
874
+ $('#get-assets-data').slideUp(function() {
875
+ if (data.error != null) {
876
+ displayError(this, data.error);
877
+ return;
878
+ }
879
+ var reportData = data.json;
880
+ var html = `
881
+ <tr>
882
+ <td><strong>Account</strong></td>
883
+ <td><strong>Balance</strong></td>
884
+ <td><strong># Transactions</strong></td>
885
+ <td><strong># Days Available</strong></td>
886
+ </tr>`;
887
+ reportData.items.forEach(function(item, itemIdx) {
888
+ item.accounts.forEach(function(account, accountIdx) {
889
+ html += '<tr>';
890
+ html += '<td>' + account.name + '</td>';
891
+ html += '<td>$' + account.balances.current + '</td>'
892
+ html += '<td>' + account.transactions.length + '</td>';
893
+ html += '<td>' + account.days_available + '</td>';
894
+ html += '</tr>';
895
+ });
896
+ });
897
+
898
+ $('#download-assets-pdf-btn')
899
+ .attr('href', `data:application/pdf;base64,${data.pdf}`)
900
+ .attr('download', 'Asset Report.pdf')
901
+ .show();
902
+
903
+ $(this).html(html).slideDown();
904
+ });
905
+ });
906
+ });
907
+
908
+ // This functionality is only relevant for the UK Payment Initiation product.
909
+ $('#get-payment-btn').on('click', function(e) {
910
+ $.get('/api/payment', function(data) {
911
+ $('#get-payment-data').slideUp(function() {
912
+ if (data.error != null) {
913
+ displayError(this, data.error);
914
+ return;
915
+ }
916
+ var paymentData = data.payment;
917
+ var html = '';
918
+ html += '<tr><td>Payment ID</td><td>' + paymentData.payment_id + '</td></tr>';
919
+ html += '<tr><td>Amount</td><td>' + (paymentData.amount.currency + ' ' + paymentData.amount.value) + '</td></tr>';
920
+ html += '<tr><td>Status</td><td>' + paymentData.status + '</td></tr>';
921
+ html += '<tr><td>Last Status Update</td><td>' + paymentData.last_status_update + '</td></tr>';
922
+ html += '<tr><td>Recipient ID</td><td>' + paymentData.recipient_id + '</td></tr>';
923
+ $(this).html(html).slideDown();
924
+ });
925
+ });
926
+ });
927
+ }
928
+ $.post('/api/info', {}, function(result) {
929
+ render_page(jQuery, result);
930
+ });
931
+
932
+ function qs(key) {
933
+ key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, "\\$&"); // escape RegEx meta chars
934
+ var match = location.search.match(new RegExp("[?&]"+key+"=([^&]+)(&|$)"));
935
+ return match && decodeURIComponent(match[1].replace(/\+/g, " "));
936
+ }
937
+
938
+ function displayError(element, error) {
939
+ var html = `
940
+ <div class="alert alert-danger">
941
+ <p><strong>Error Code:</strong> ${error.error_code}</p>
942
+ <p><strong>Error Type:</strong> ${error.error_type}</p>
943
+ <p><strong>Error Message:</strong> ${error.display_message == null ? error.error_message : error.display_message}</p>
944
+ <div>Check out our <a href="https://plaid.com/docs/#errors-overview">errors documentation</a> for more information.</div>
945
+ </div>`;
946
+ $(element).html(html).slideDown();
947
+ }
948
+ </script>
949
+ </body>
950
+ </html>