Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Migrating from `v1.x.x`](#migrating-from-v1xx)
- [Importing Account Balances & Transactions](#importing-account-balances--transactions)
- [Automatically – in the cloud – via Plaid](#automatically-in-the-cloud--via-plaid)
- [Automatically – via Teller](#automatically-via-teller)
- [Manually – on your local machine – via CSV bank statements](#manually--on-your-local-machine--via-csv-bank-statements)
- [Exporting Account Balances & Transactions](#exporting-account-balances--transactions)
- [In the cloud – via Google Sheets](#in-the-cloud-via-google-sheets)
Expand Down Expand Up @@ -116,6 +117,32 @@ To add a new account, click the blue **Link A New Account** button. To re-authen

> **Note:** Plaid is the default import integration and these steps are not necessary if you've already run `mintable setup`.

### Automatically via [Teller](https://teller.io)

You can run:

```bash
mintable teller-setup
```

to enter the Teller setup wizard. This will ask for things like the certificate, private key, and application ID provided when you sign up for Teller.

After you have the base Teller integration working, you can run:

```bash
mintable teller-account-setup
```

to enter the account setup wizard to add or remove accounts.

This will launch a local web server (necessary to authenticate with Teller's servers) for you to connect your banks.

To add a new account, click the blue **Link A New Account** button.

> **Note:** Access to an account may expire. In that case, you should run `mintable teller-account-setup` to re-add the accounts with expired access.

After set up is complete, you will import updated account balances/transactions from your banking institutions every time `mintable fetch` is run.

### Manually – on your local machine – via CSV bank statements

You can run:
Expand Down
9 changes: 9 additions & 0 deletions docs/css/account-setup.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ button.remove:hover {
background-color: #ed0d3a;
}

button.loading,
button.loading:hover,
button.remove.loading:hover {
background: #fff;
border-color: #aaa;
color: #aaa;
cursor: wait;
}

#link-button {
margin-top: 80px;
}
Expand Down
128 changes: 128 additions & 0 deletions src/integrations/teller/account-setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<html>
<head>
<link rel="stylesheet" href="/css/account-setup.css" />
</head>
<body>
<div id="container">
<img id="logo" src="/img/icon.png" alt="Mintable Logo" />
<h1>Mintable</h1>
<h2>Teller Account Setup</h2>
<div id="accounts"><div id="accounts-table"></div></div>

<div id="add-accounts" style="display:none">
<h3>Accounts Available to Add</h3>
<div id="add-accounts-table"></div>
</div>

<button id="link-button">Link A New Account</button>

<br />

<button id="done-button">Done Linking Accounts</button>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<script src="https://cdn.teller.io/connect/connect.js"></script>

<script type="text/javascript">
;(function($) {
let tellerConnect
let currentAccessToken
let params = new URLSearchParams(window.location.search)
const accountIds = []

// Load and display any accounts using Teller that are already configured.
$.post('/accounts').then(accounts => {
if (accounts.length == 0) {
const text = `<p>No Teller accounts set up yet. Click "Link A New Account" to add one.`
$('#accounts-table').append(text)
} else {
const text = `<h3>Current Accounts</h3>`
$('#accounts').prepend(text)
const table = `<table><tr><th>Teller ID</th><th>Name</th><th>Remove</th></tr></table>`
$('#accounts-table').append(table)

accounts.forEach(account => {
accountIds.push(account.id)
const removeButton = `<button class="remove" data-account-id="${account.id}">Remove</button>`
const row = `<tr><td>${account.id}</td><td>${account.name}</td><td>${removeButton}</td></tr>`
$('#accounts table').append(row)
})
}
})

// Handle the Add button.
$(document).on('click', 'button.add', function(e) {
const accountId = e.currentTarget.getAttribute('data-account-id')
e.currentTarget.classList.add('loading')
e.currentTarget.textContent = 'Adding...'
$.post('/register_account', { accessToken: currentAccessToken, accountId }).then(() => {
setTimeout(() => location.reload(), 100)
})
})

// Handle the Remove button.
$(document).on('click', 'button.remove', function(e) {
const confirmed = confirm('Are you sure you want to remove this account? This cannot be undone.')
if (confirmed === true) {
const accountId = e.currentTarget.getAttribute('data-account-id')
e.currentTarget.classList.add('loading')
e.currentTarget.textContent = 'Removing...'
$.post('/remove', { accountId }).then(setTimeout(() => location.reload(), 1000))
}
})

// Handle the Link A New Account button.
$('#link-button').on('click', function(e) {
tellerConnect.open()
})

// Handle the Done Linking Accounts button.
$('#done-button').on('click', function(e) {
$.post('/done').then(data => {
$('#container').text('You can now close this page in your browser.')
})
})

// Handle when accounts associated with a user enrollment are received.
const onEnrollmentAccounts = acc => {
const table = `<table><tr><th>Teller ID</th><th>Name</th><th>Add</th></tr></table>`
$('#add-accounts-table').html(table)

let accToAdd = 0
acc.forEach(account => {
if (accountIds.includes(account.id)) return
const addButton = `<button class="add" data-account-id="${account.id}">Add</button>`
const row = `<tr><td>${account.id}</td><td>${account.institution?.name} ${account.name} ${account.last_four}</td><td>${addButton}</td></tr>`
$('#add-accounts table').append(row)
accToAdd++
})

if (!accToAdd) {
const text = `<p>There are no accounts found to add from this institution.</p>`
$('#add-accounts-table').replaceWith(text)
}

$('#add-accounts').show()
}

// Initialize the client side connection with Teller for account linking.
tellerConnect = TellerConnect.setup({
applicationId: params.get('tellerAppId'),
onInit: function() {
console.log('Teller Connect has initialized')
},
onSuccess: function(enrollment) {
// When the user authenticates with a bank and receives an
// access token, load the accounts associated with the token.
currentAccessToken = enrollment.accessToken
$.post('/get_enrollment_accounts', { enrollment }).then(onEnrollmentAccounts)
},
onExit: function() {
console.log('User closed Teller Connect')
}
})
})(jQuery)
</script>
</body>
</html>
32 changes: 32 additions & 0 deletions src/integrations/teller/accountSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getConfig } from '../../common/config'
import { logInfo, logError } from '../../common/logging'
import open from 'open'
import { TellerIntegration } from './tellerIntegration'
import { IntegrationId } from '../../types/integrations'
import { TellerConfig } from '../../types/integrations/teller'

export default () => {
return new Promise((resolve, reject) => {
try {
console.log('\nThis script will help you add accounts using Teller.\n')
console.log('\n\t1. A page will open in your browser allowing you to link accounts with Teller.')
console.log('\t2. Sign in with your banking provider for each account you wish to link.')
console.log('\t3. Click \'Done Linking Accounts\' in your browser when you are finished.\n')

const config = getConfig()
const tellerConfig = config.integrations[IntegrationId.Teller] as TellerConfig
const teller = new TellerIntegration(config)

logInfo('Account setup in progress.')
open(`http://localhost:8000?tellerAppId=${tellerConfig.appId}`)
teller.accountSetup()
.then(() => {
logInfo('Successfully set up Teller Account(s).')
return resolve(true)
})
} catch (e) {
logError('Unable to set up Teller Account(s).', e)
return reject()
}
})
}
66 changes: 66 additions & 0 deletions src/integrations/teller/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { existsSync, realpathSync } from 'fs'
import prompts from 'prompts'

import { TellerConfig, defaultTellerConfig } from '../../types/integrations/teller'
import { updateConfig } from '../../common/config'
import { IntegrationId } from '../../types/integrations'
import { logInfo, logError } from '../../common/logging'

export default async () => {
try {
console.log('\nThis script will walk you through setting up the Teller integration. Follow these steps:')
console.log('\n\t1. Visit https://teller.io')
console.log('\t2. Click \'Get Started\'')
console.log('\t3. Fill out the form')
console.log('\t4. Find the application ID, and download the certificate and private key')
console.log('\t5. Answer the following questions:\n')

const credentials = await prompts([
{
type: 'text',
name: 'name',
message: 'What would you like to call this integration?',
initial: 'Teller',
validate: (s: string) =>
1 < s.length && s.length <= 64 ? true : 'Must be between 2 and 64 characters in length.'
},
{
type: 'text',
name: 'pathCertificate',
message: 'Path to Certificate',
validate: (s: string) => s.length && existsSync(s) ? true : 'Must enter path to certificate file.'
},
{
type: 'text',
name: 'pathPrivateKey',
message: 'Path to Private Key',
validate: (s: string) => s.length && existsSync(s) ? true : 'Must enter path to private key file.'
},
{
type: 'text',
name: 'appId',
message: 'Application ID',
validate: (s: string) => s.length && s.startsWith('app_') ? true : 'Must enter Application ID from Teller.'
}
])

updateConfig(config => {
const tellerConfig = (config.integrations[IntegrationId.Teller] as TellerConfig) || defaultTellerConfig

tellerConfig.name = credentials.name
tellerConfig.pathCertificate = realpathSync(credentials.pathCertificate)
tellerConfig.pathPrivateKey = realpathSync(credentials.pathPrivateKey)
tellerConfig.appId = credentials.appId

config.integrations[IntegrationId.Teller] = tellerConfig

return config
})

logInfo('Successfully set up Teller Integration.')
return true
} catch (e) {
logError('Unable to set up Teller Integration.', e)
return false
}
}
Loading