ynap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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>