Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
cb28b55617 | |||
12614ff011 | |||
7274f7859a | |||
068198a02f | |||
62537fffe2 | |||
22239ddbeb | |||
9b7f25c996 | |||
7637fca672 | |||
d7ba62e767 | |||
019197e43c | |||
596f4e5398 | |||
dca6f9ef0b | |||
bdd7419d3d | |||
0d99ee51d5 | |||
0f80623111 | |||
6c02861a79 | |||
a25e758faf | |||
14f2ba0692 | |||
0c391c1fd1 | |||
0c9decce3e | |||
edb3160f35 | |||
0eb51cf3c5 | |||
80373424b4 | |||
d108baf672 | |||
031d140a44 | |||
c96ccf198c | |||
18835fa5ae | |||
c0e26cdc4b | |||
68a3fcb06f | |||
2433b0d7b2 | |||
570a1cd6b2 | |||
9425a85150 | |||
2b902aa31b | |||
7bb93a5edf |
@ -100,10 +100,9 @@ codequality:
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g tslint typescript
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
|
27512
package-lock.json
generated
27512
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@mojoio/tink",
|
||||
"version": "1.0.6",
|
||||
"version": "3.1.3",
|
||||
"private": false,
|
||||
"description": "an unofficial api abstraction for tink.com",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@ -12,17 +13,19 @@
|
||||
"build": "(tsbuild --web)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.25",
|
||||
"@gitzone/tsbundle": "^1.0.78",
|
||||
"@gitzone/tstest": "^1.0.64",
|
||||
"@pushrocks/qenv": "^4.0.10",
|
||||
"@pushrocks/tapbundle": "^4.0.7",
|
||||
"@types/node": "^17.0.18",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.15.0"
|
||||
"@gitzone/tsbuild": "^2.1.65",
|
||||
"@gitzone/tsbundle": "^2.0.7",
|
||||
"@gitzone/tsrun": "^1.2.39",
|
||||
"@gitzone/tstest": "^1.0.73",
|
||||
"@pushrocks/qenv": "^5.0.2",
|
||||
"@pushrocks/tapbundle": "^5.0.4",
|
||||
"@types/node": "^18.11.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pushrocks/smartrequest": "^1.1.56"
|
||||
"@pushrocks/smartdelay": "^2.0.13",
|
||||
"@pushrocks/smartpromise": "^3.1.7",
|
||||
"@pushrocks/smartrequest": "^2.0.10",
|
||||
"@pushrocks/smarturl": "^3.0.5"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
|
4353
pnpm-lock.yaml
generated
Normal file
4353
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
readme.md
24
readme.md
@ -27,6 +27,30 @@ Platform support | [;
|
||||
const tinkUser = await tinkAccount.createTinkUser('<YourOwnUniqueUserId/externalUserId>');
|
||||
const tinkLinkUrl = await tinkUser.getTinkLink('<marketCode like DE>');
|
||||
|
||||
// present the link to your user to connect their bank accounts to the tink platform.
|
||||
|
||||
const tinkProviderConsents = await tinkUser.getProviderConsents();
|
||||
|
||||
for (const providerConsent of tinkProviderConsents) {
|
||||
const bankAccounts = await providerConsent.getBankAccounts();
|
||||
for (const bankAccount of bankAccounts) {
|
||||
const transactions = bankAccount.getTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
// additional stuff
|
||||
const existingTinkUser = await tinkAccount.getUser('<YourOwnUniqueUserId/externalUserId>');
|
||||
await existingTinkuser.delete(); // delete the user on the tink platform
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
|
@ -3,20 +3,10 @@ import * as qenv from '@pushrocks/qenv';
|
||||
|
||||
const testQenv = new qenv.Qenv('./', './.nogit/');
|
||||
|
||||
import * as tink from '../ts/index';
|
||||
import * as tink from '../ts/index.js';
|
||||
|
||||
let tinkTestAccount: tink.TinkAccount;
|
||||
|
||||
tap.preTask('should delete existing users', async () => {
|
||||
const preTinkAccount = tinkTestAccount = new tink.TinkAccount(
|
||||
testQenv.getEnvVarOnDemand('TINK_CLIENT_ID'),
|
||||
testQenv.getEnvVarOnDemand('TINK_CLIENT_SECRET')
|
||||
);
|
||||
expect(tinkTestAccount).toBeInstanceOf(tink.TinkAccount);
|
||||
const tinkUser = new tink.TinkUser(preTinkAccount, null, 'user_1234_abc');
|
||||
await tinkUser.delete();
|
||||
})
|
||||
|
||||
tap.test('should create a valid tink account', async () => {
|
||||
tinkTestAccount = new tink.TinkAccount(
|
||||
testQenv.getEnvVarOnDemand('TINK_CLIENT_ID'),
|
||||
@ -29,8 +19,46 @@ tap.test('should report tink as healthy', async () => {
|
||||
await expectAsync(tinkTestAccount.getTinkHealthyBoolean()).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should create a valid request', async () => {
|
||||
await tinkTestAccount.createTinkUser("user_1234_abc");
|
||||
tap.test('should create a tink user', async (toolsArg) => {
|
||||
await tinkTestAccount.createTinkUser('user_1234_abc');
|
||||
})
|
||||
|
||||
tap.test('should create a valid request', async (toolsArg) => {
|
||||
const tinkuser: tink.TinkUser = await tinkTestAccount.getTinkUser('user_1234_abc');
|
||||
console.log(tinkuser);
|
||||
console.log(await tinkuser.getTinkLinkForMarket()); // defaults to 'DE';
|
||||
console.log(await tinkuser.getProviderConsents());
|
||||
});
|
||||
|
||||
tap.test('allow tink link to be used', async (toolsArg) => {
|
||||
await toolsArg.delayFor(30000);
|
||||
});
|
||||
|
||||
tap.test('get provider consents', async () => {
|
||||
const tinkuser: tink.TinkUser = await tinkTestAccount.getTinkUser('user_1234_abc');
|
||||
const providerConsents = await tinkuser.getProviderConsents();
|
||||
console.log(providerConsents);
|
||||
});
|
||||
|
||||
tap.test('get bankaccounts', async (toolsArg) => {
|
||||
const tinkuser: tink.TinkUser = await tinkTestAccount.getTinkUser('user_1234_abc');
|
||||
const bankAccounts = await tinkuser.getAllBankAccounts();
|
||||
console.log(bankAccounts.map(bankAccountArg => bankAccountArg.getNormalizedData()));
|
||||
|
||||
for (const bankAccount of bankAccounts) {
|
||||
const transactions = await bankAccount.getTransactions();
|
||||
for (const transaction of transactions) {
|
||||
console.log(`=======================`)
|
||||
console.log(JSON.stringify(transaction.getNormalizedData()));
|
||||
}
|
||||
await toolsArg.delayFor(10000);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should delete existing users', async () => {
|
||||
expect(tinkTestAccount).toBeInstanceOf(tink.TinkAccount);
|
||||
const tinkUser = new tink.TinkUser(tinkTestAccount, null, 'user_1234_abc');
|
||||
await tinkUser.delete();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@mojoio/tink',
|
||||
version: '3.1.3',
|
||||
description: 'an unofficial api abstraction for tink.com'
|
||||
}
|
1
ts/helpers/index.ts
Normal file
1
ts/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './tinkmath.js';
|
22
ts/helpers/tinkmath.ts
Normal file
22
ts/helpers/tinkmath.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export interface ITinkScaledAmount {
|
||||
value: {
|
||||
unscaledValue: string;
|
||||
scale: string;
|
||||
};
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a normalized amount
|
||||
* @param scaledArg
|
||||
* @returns
|
||||
*/
|
||||
export const getNormalizedAmount = (scaledArg?: ITinkScaledAmount) => {
|
||||
if (!scaledArg) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
amount: parseInt(scaledArg.value.unscaledValue) * Math.pow(10, -(parseInt(scaledArg.value.scale))),
|
||||
currency: 'EUR'
|
||||
};
|
||||
};
|
@ -1,2 +1,3 @@
|
||||
export * from './tink.classes.tinkaccount';
|
||||
export * from './tink.classes.tinkuser';
|
||||
export * from './tink.classes.tinkaccount.js';
|
||||
export * from './tink.classes.tinkuser.js';
|
||||
export * from './tink.classes.tinkproviderconsent.js';
|
130
ts/tink.classes.bankaccount.ts
Normal file
130
ts/tink.classes.bankaccount.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { BankTransaction } from './tink.classes.banktransaction.js';
|
||||
import { TinkUser } from './tink.classes.tinkuser.js';
|
||||
import * as plugins from './tink.plugins.js';
|
||||
|
||||
import * as tinkHelpers from './helpers/index.js';
|
||||
|
||||
export interface ITinkBankAccountData {
|
||||
balances: {
|
||||
booked: {
|
||||
amount: {
|
||||
currencyCode: string;
|
||||
value: {
|
||||
scale: string;
|
||||
unscaledValue: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
available: {
|
||||
amount: {
|
||||
currencyCode: string;
|
||||
value: {
|
||||
scale: string;
|
||||
unscaledValue: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
customerSegment: string;
|
||||
dates: {
|
||||
lastRefreshed: string;
|
||||
};
|
||||
financialInstitutionId: string;
|
||||
id: string;
|
||||
identifiers: {
|
||||
iban?: {
|
||||
bban: string;
|
||||
iban: string;
|
||||
};
|
||||
pan: {
|
||||
masked: string;
|
||||
};
|
||||
financialInstitution: { accountNumber: string; referenceNumbers: unknown };
|
||||
};
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class BankAccount {
|
||||
// STATIC
|
||||
public static async getAccountUserAccessToken(tinkUserArg: TinkUser) {
|
||||
const authorizationCode = await tinkUserArg.tinkAccountRef.getUserAuthorizationCode(
|
||||
tinkUserArg.externalUserIdArg,
|
||||
tinkUserArg.tinkAccountRef.clientId,
|
||||
'accounts:read,balances:read,transactions:read,provider-consents:read'
|
||||
);
|
||||
const accessToken = await tinkUserArg.tinkAccountRef.getUserAccessToken(authorizationCode);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public static async getBankAccountsForUser(tinkUserArg: TinkUser) {
|
||||
const userAccessToken = await this.getAccountUserAccessToken(tinkUserArg);
|
||||
const returnBankAccounts: BankAccount[] = [];
|
||||
const getBankAccountRecursively = async (nextPageToken?: string) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('pageSize', '200');
|
||||
if (nextPageToken) {
|
||||
searchParams.set('pageToken', nextPageToken);
|
||||
}
|
||||
const response = await tinkUserArg.tinkAccountRef.request({
|
||||
urlArg: `/data/v2/accounts?${searchParams.toString()}`,
|
||||
accessToken: userAccessToken,
|
||||
methodArg: 'GET',
|
||||
payloadArg: null,
|
||||
});
|
||||
for (const account of response.accounts) {
|
||||
returnBankAccounts.push(new BankAccount(tinkUserArg, account));
|
||||
}
|
||||
if (response.nextPageToken.length > 0) {
|
||||
await getBankAccountRecursively(response.nextPageToken);
|
||||
}
|
||||
};
|
||||
await getBankAccountRecursively();
|
||||
return returnBankAccounts;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
tinkUserRef: TinkUser;
|
||||
data: ITinkBankAccountData;
|
||||
constructor(tinkUserRefArg: TinkUser, dataArg: ITinkBankAccountData) {
|
||||
this.tinkUserRef = tinkUserRefArg;
|
||||
this.data = dataArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the account and tries to get the latest state from bunq
|
||||
*/
|
||||
public async update() {
|
||||
const bankAccounts = await BankAccount.getBankAccountsForUser(this.tinkUserRef);
|
||||
const matchingAccount = bankAccounts.find(
|
||||
(bankAccountArg) => bankAccountArg.data.id === this.data.id
|
||||
);
|
||||
if (matchingAccount) {
|
||||
this.data = matchingAccount.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets normalized data
|
||||
*/
|
||||
public getNormalizedData() {
|
||||
return {
|
||||
id: this.data.id,
|
||||
name: this.data.name,
|
||||
accountNumber:
|
||||
this.data.identifiers.iban?.iban ||
|
||||
this.data.identifiers?.financialInstitution?.accountNumber ||
|
||||
null,
|
||||
bookedValue: tinkHelpers.getNormalizedAmount(this.data.balances.booked?.amount),
|
||||
availableValue: tinkHelpers.getNormalizedAmount(this.data.balances.available?.amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the transactions for the bank account
|
||||
*/
|
||||
public async getTransactions() {
|
||||
const transactions = await BankTransaction.getBankTransactions(this);
|
||||
return transactions;
|
||||
}
|
||||
}
|
114
ts/tink.classes.banktransaction.ts
Normal file
114
ts/tink.classes.banktransaction.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { BankAccount } from './tink.classes.bankaccount.js';
|
||||
import * as plugins from './tink.plugins.js';
|
||||
|
||||
import * as tinkHelpers from './helpers/index.js';
|
||||
|
||||
export interface ITinkBankTransactiondata {
|
||||
id: string;
|
||||
accountId:string;
|
||||
amount: {
|
||||
currencyCode: string;
|
||||
value: {
|
||||
scale: string;
|
||||
unscaledValue: string;
|
||||
};
|
||||
};
|
||||
categories: {
|
||||
pfm: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
dates: {
|
||||
booked: string;
|
||||
value: string;
|
||||
};
|
||||
descriptions: {
|
||||
display: string;
|
||||
original: string;
|
||||
};
|
||||
identifiers: {
|
||||
providerTransactionId: string;
|
||||
};
|
||||
merchantInformation: {
|
||||
merchantCategoryCode: 'string';
|
||||
merchantName: 'string';
|
||||
};
|
||||
providerMutability: string;
|
||||
reference: string;
|
||||
status: string;
|
||||
types: {
|
||||
financialInstitutionTypeCode: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class BankTransaction {
|
||||
// STATIC
|
||||
public static async getTransactionAccessToken(bankAccountArg: BankAccount) {
|
||||
const authorizationCode =
|
||||
await bankAccountArg.tinkUserRef.tinkAccountRef.getUserAuthorizationCode(
|
||||
bankAccountArg.tinkUserRef.externalUserIdArg,
|
||||
bankAccountArg.tinkUserRef.tinkAccountRef.clientId,
|
||||
'accounts:read,balances:read,transactions:read,provider-consents:read'
|
||||
);
|
||||
const accessToken = await bankAccountArg.tinkUserRef.tinkAccountRef.getUserAccessToken(
|
||||
authorizationCode
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public static async getBankTransactions(bankAccountArg: BankAccount) {
|
||||
const accessToken = await this.getTransactionAccessToken(bankAccountArg);
|
||||
const pageSize = 100;
|
||||
const returnTransactions: BankTransaction[] = [];
|
||||
const getTransactionsRecursively = async (nextPageToken?: string) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('accountIdIn', bankAccountArg.data.id);
|
||||
searchParams.set('pageSize', '200');
|
||||
if (nextPageToken) {
|
||||
searchParams.set('pageToken', nextPageToken)
|
||||
}
|
||||
const response = await bankAccountArg.tinkUserRef.tinkAccountRef.request({
|
||||
urlArg: `/data/v2/transactions?${searchParams.toString()}`,
|
||||
accessToken: accessToken,
|
||||
methodArg: 'GET',
|
||||
payloadArg: null,
|
||||
});
|
||||
for (const transaction of response.transactions) {
|
||||
returnTransactions.push(new BankTransaction(bankAccountArg, transaction));
|
||||
}
|
||||
|
||||
if (response.nextPageToken.length > 0) {
|
||||
await getTransactionsRecursively(response.nextPageToken);
|
||||
}
|
||||
};
|
||||
await getTransactionsRecursively();
|
||||
return returnTransactions;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
bankAccountRef: BankAccount;
|
||||
data: ITinkBankTransactiondata;
|
||||
|
||||
constructor(bankAccountRefArg: BankAccount, dataArg: ITinkBankTransactiondata) {
|
||||
this.bankAccountRef = bankAccountRefArg;
|
||||
this.data = dataArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets normalized data
|
||||
*/
|
||||
public getNormalizedData() {
|
||||
return {
|
||||
id: this.data.id,
|
||||
amount: tinkHelpers.getNormalizedAmount(this.data.amount),
|
||||
name: this.data.descriptions.display,
|
||||
description: this.data.descriptions.original,
|
||||
justForLooks: {
|
||||
originalScaledAmount: this.data.amount
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,45 +1,35 @@
|
||||
import * as plugins from './tink.plugins';
|
||||
import * as plugins from './tink.plugins.js';
|
||||
|
||||
import { TinkUser } from './tink.classes.tinkuser'
|
||||
import { TinkUser } from './tink.classes.tinkuser.js';
|
||||
|
||||
export class TinkAccount {
|
||||
private clientId: string;
|
||||
private clientSecret: string;
|
||||
public clientId: string;
|
||||
private _clientSecret: string;
|
||||
|
||||
private apiBaseUrl: string = 'https://api.tink.com';
|
||||
private _apiBaseUrl: string = 'https://api.tink.com';
|
||||
|
||||
constructor(clientIdArg: string, clientSecretArg: string) {
|
||||
this.clientId = clientIdArg;
|
||||
this.clientSecret = clientSecretArg;
|
||||
this._clientSecret = clientSecretArg;
|
||||
}
|
||||
|
||||
public async getTinkHealthyBoolean(): Promise<boolean> {
|
||||
const response = await plugins.smartrequest.request(
|
||||
'https://api.tink.com/api/v1/monitoring/healthy',
|
||||
{}
|
||||
{
|
||||
keepAlive: false,
|
||||
}
|
||||
);
|
||||
return response.body === 'ok';
|
||||
}
|
||||
|
||||
// the request method for tink respecting platform specific stuff
|
||||
// e.g. certain headers if needed
|
||||
public async request(optionsArg: {
|
||||
urlArg: string,
|
||||
methodArg: 'POST' | 'GET',
|
||||
scopeArg: string,
|
||||
payloadArg: any,
|
||||
externalUserId: string
|
||||
}) {
|
||||
// check health
|
||||
if (!(await this.getTinkHealthyBoolean())) {
|
||||
throw new Error('TINK is not healthy right now. Please try again later.');
|
||||
} else {
|
||||
console.log('tink is healthy, continuing...');
|
||||
}
|
||||
public async getClientAccessTokenForScope(scopeArg: string): Promise<string> {
|
||||
// lets get an accessToken for the request
|
||||
const response = await plugins.smartrequest.postFormDataUrlEncoded(
|
||||
`${this.apiBaseUrl}/api/v1/oauth/token`,
|
||||
{},
|
||||
`${this._apiBaseUrl}/api/v1/oauth/token`,
|
||||
{
|
||||
keepAlive: false,
|
||||
},
|
||||
[
|
||||
{
|
||||
key: 'client_id',
|
||||
@ -47,7 +37,7 @@ export class TinkAccount {
|
||||
},
|
||||
{
|
||||
key: 'client_secret',
|
||||
content: this.clientSecret,
|
||||
content: this._clientSecret,
|
||||
},
|
||||
{
|
||||
key: 'grant_type',
|
||||
@ -55,28 +45,134 @@ export class TinkAccount {
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
content: optionsArg.scopeArg,
|
||||
content: scopeArg,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (response.statusCode !== 200) {
|
||||
console.log(response.statusCode);
|
||||
console.log(response.body);
|
||||
throw new Error('there was an error aquiring an access token.');
|
||||
}
|
||||
const accessToken = response.body.access_token;
|
||||
|
||||
const response2 = await plugins.smartrequest.request(`${this.apiBaseUrl}${optionsArg.urlArg}`, {
|
||||
const clientAccessToken = response.body.access_token;
|
||||
return clientAccessToken;
|
||||
}
|
||||
|
||||
public async getUserAuthorizationCode(
|
||||
externalUserIdArg: string,
|
||||
actorCLientIdArg: string,
|
||||
scopeArg: string
|
||||
) {
|
||||
const accessToken = await this.getClientAccessTokenForScope('authorization:grant');
|
||||
const response = await plugins.smartrequest.postFormDataUrlEncoded(
|
||||
`${this._apiBaseUrl}/api/v1/oauth/authorization-grant/delegate`,
|
||||
{
|
||||
keepAlive: false,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
key: 'response_type',
|
||||
content: 'code',
|
||||
},
|
||||
{
|
||||
key: 'actor_client_id',
|
||||
content: actorCLientIdArg,
|
||||
},
|
||||
{
|
||||
key: 'external_user_id',
|
||||
content: externalUserIdArg,
|
||||
},
|
||||
{
|
||||
key: 'id_hint',
|
||||
content: 'Hello there',
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
content: scopeArg,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.log(response.body);
|
||||
throw new Error('there was an error aquiring an access token.');
|
||||
}
|
||||
const userAuthorizationCode = response.body.code;
|
||||
return userAuthorizationCode;
|
||||
}
|
||||
|
||||
public async getUserAccessToken(authorizationCode: string): Promise<string> {
|
||||
const accessToken = await this.getClientAccessTokenForScope('authorization:grant');
|
||||
const response = await plugins.smartrequest.postFormDataUrlEncoded(
|
||||
`${this._apiBaseUrl}/api/v1/oauth/token`,
|
||||
{
|
||||
keepAlive: false,
|
||||
},
|
||||
[
|
||||
{
|
||||
key: 'code',
|
||||
content: authorizationCode,
|
||||
},
|
||||
{
|
||||
key: 'client_id',
|
||||
content: this.clientId,
|
||||
},
|
||||
{
|
||||
key: 'client_secret',
|
||||
content: this._clientSecret,
|
||||
},
|
||||
{
|
||||
key: 'grant_type',
|
||||
content: 'authorization_code',
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.log(response.body);
|
||||
throw new Error('there was an error aquiring an access token.');
|
||||
}
|
||||
const userAccessToken = response.body.access_token;
|
||||
return userAccessToken;
|
||||
}
|
||||
|
||||
// the request method for tink respecting platform specific stuff
|
||||
// e.g. certain headers if needed
|
||||
public async request(optionsArg: {
|
||||
urlArg: string;
|
||||
methodArg: 'POST' | 'GET';
|
||||
accessToken: string;
|
||||
payloadArg: any;
|
||||
}) {
|
||||
// check health
|
||||
if (!(await this.getTinkHealthyBoolean())) {
|
||||
throw new Error('TINK is not healthy right now. Please try again later.');
|
||||
} else {
|
||||
console.log('tink is healthy, continuing...');
|
||||
}
|
||||
const response = await plugins.smartrequest.request(`${this._apiBaseUrl}${optionsArg.urlArg}`, {
|
||||
keepAlive: false,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
Authorization: `Bearer ${optionsArg.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: optionsArg.methodArg,
|
||||
requestBody: JSON.stringify(optionsArg.payloadArg)
|
||||
})
|
||||
console.log(response2.statusCode);
|
||||
return response2.body;
|
||||
requestBody: JSON.stringify(optionsArg.payloadArg),
|
||||
});
|
||||
console.log(response.statusCode);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
public async getTinkUser(externalUserIdArg: string) {
|
||||
const tinkuser = await TinkUser.getTinkUser(this, externalUserIdArg);
|
||||
return tinkuser;
|
||||
}
|
||||
|
||||
public async createTinkUser(externalUserIdArg: string) {
|
||||
const tinkuser = await TinkUser.createNewTinkUser(this, externalUserIdArg);
|
||||
return tinkuser;
|
||||
}
|
||||
}
|
||||
|
87
ts/tink.classes.tinkproviderconsent.ts
Normal file
87
ts/tink.classes.tinkproviderconsent.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as plugins from './tink.plugins.js';
|
||||
|
||||
export interface IProviderData {
|
||||
credentialsId: string;
|
||||
providerName: string;
|
||||
status: string;
|
||||
sessionExpiryDate: number;
|
||||
accountIds: string[];
|
||||
statusUpdated: number;
|
||||
}
|
||||
|
||||
import { TinkUser } from './tink.classes.tinkuser.js';
|
||||
|
||||
/**
|
||||
* a provider consent maps to tinks bank consents
|
||||
*/
|
||||
export class TinkProviderConsent {
|
||||
public static async getProviderUserAccessToken(tinkUserRefArg: TinkUser) {
|
||||
const authorizationCode = await tinkUserRefArg.tinkAccountRef.getUserAuthorizationCode(
|
||||
tinkUserRefArg.externalUserIdArg,
|
||||
tinkUserRefArg.tinkAccountRef.clientId,
|
||||
'accounts:read,balances:read,transactions:read,provider-consents:read'
|
||||
);
|
||||
const accessToken = await tinkUserRefArg.tinkAccountRef.getUserAccessToken(authorizationCode);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
// STATIC
|
||||
public static async getProviderConsentsForUser(tinkUserRefArg: TinkUser) {
|
||||
const returnProviderConsents: TinkProviderConsent[] = [];
|
||||
const accessToken = await this.getProviderUserAccessToken(tinkUserRefArg);
|
||||
const responseData = await tinkUserRefArg.tinkAccountRef.request({
|
||||
urlArg: '/api/v1/provider-consents',
|
||||
accessToken,
|
||||
methodArg: 'GET',
|
||||
payloadArg: null,
|
||||
});
|
||||
// console.log(responseData); // no nextPageToken here?
|
||||
if (responseData.providerConsents) {
|
||||
for (const providerConsentData of responseData.providerConsents) {
|
||||
returnProviderConsents.push(new TinkProviderConsent(tinkUserRefArg, providerConsentData));
|
||||
}
|
||||
}
|
||||
return returnProviderConsents;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
tinkUserRef: TinkUser;
|
||||
data: IProviderData;
|
||||
constructor(tinkUserRefArg: TinkUser, dataArg: IProviderData) {
|
||||
this.tinkUserRef;
|
||||
this.data = dataArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* refresh the bank account data from origin
|
||||
*/
|
||||
public async refresh() {
|
||||
const userAccessToken = await TinkProviderConsent.getProviderUserAccessToken(this.tinkUserRef);
|
||||
const response = await this.tinkUserRef.tinkAccountRef.request({
|
||||
accessToken: userAccessToken,
|
||||
methodArg: 'POST',
|
||||
urlArg: `https://api.tink.com/api/v1/credentials/${this.data.credentialsId}/refresh`,
|
||||
payloadArg: null
|
||||
});
|
||||
|
||||
// polling as per documentation for status 'UPDATED';
|
||||
let pollingRounds = 0;
|
||||
let status = null;
|
||||
while(status !== 'UPDATED' && pollingRounds < 10) {
|
||||
await plugins.smartdelay.delayFor(2000);
|
||||
await this.update();
|
||||
status = this.data.status;
|
||||
pollingRounds++;
|
||||
}
|
||||
// think about how to handle errors here
|
||||
}
|
||||
|
||||
public async update() {
|
||||
const providerConsents = await TinkProviderConsent.getProviderConsentsForUser(this.tinkUserRef);
|
||||
for (const providerConsent of providerConsents) {
|
||||
if (providerConsent.data.credentialsId === this.data.credentialsId) {
|
||||
this.data = providerConsent.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,79 @@
|
||||
import * as plugins from './tink.plugins';
|
||||
import * as plugins from './tink.plugins.js';
|
||||
|
||||
import { TinkAccount } from './tink.classes.tinkaccount';
|
||||
import { TinkAccount } from './tink.classes.tinkaccount.js';
|
||||
import { TinkProviderConsent } from './tink.classes.tinkproviderconsent.js';
|
||||
import { BankAccount } from './tink.classes.bankaccount.js';
|
||||
|
||||
export class TinkUser {
|
||||
// STATIC
|
||||
public static async createNewTinkUser (tinkaccountArg: TinkAccount, externalUserIdArg: string) {
|
||||
const response = await tinkaccountArg.request({
|
||||
externalUserId: null,
|
||||
public static async createNewTinkUser(tinkAccountArg: TinkAccount, externalUserIdArg: string) {
|
||||
const accessToken = await tinkAccountArg.getClientAccessTokenForScope('user:create');
|
||||
const responseData: {
|
||||
external_user_id: string;
|
||||
user_id: string;
|
||||
} = await tinkAccountArg.request({
|
||||
urlArg: '/api/v1/user/create',
|
||||
accessToken,
|
||||
methodArg: 'POST',
|
||||
scopeArg: 'user:create',
|
||||
payloadArg: {
|
||||
"external_user_id": externalUserIdArg,
|
||||
"market": "GB",
|
||||
"locale": "en_US"
|
||||
}
|
||||
external_user_id: externalUserIdArg,
|
||||
market: 'DE',
|
||||
locale: 'en_US',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
const newTinkUser = await TinkUser.getTinkUser(tinkAccountArg, externalUserIdArg);
|
||||
return newTinkUser;
|
||||
}
|
||||
|
||||
public static async getTinkUser(tinkAccountArg: TinkAccount, externalUserIdArg: string) {
|
||||
const authorizationCode = await tinkAccountArg.getUserAuthorizationCode(
|
||||
externalUserIdArg,
|
||||
tinkAccountArg.clientId,
|
||||
'user:read'
|
||||
);
|
||||
const accessToken = await tinkAccountArg.getUserAccessToken(authorizationCode);
|
||||
const responseData: {
|
||||
appId: string;
|
||||
created: string;
|
||||
externalUserId: string;
|
||||
flags: string[];
|
||||
id: string;
|
||||
nationalId: string;
|
||||
profile: {
|
||||
// cashbackEnabled: boolean; // deprecated
|
||||
currency: string;
|
||||
locale: string;
|
||||
market: string;
|
||||
notificationSettings: {
|
||||
balance: boolean;
|
||||
budget: boolean;
|
||||
doubleCharge: boolean;
|
||||
einvoices: boolean;
|
||||
fraud: boolean;
|
||||
income: boolean;
|
||||
largeExpense: boolean;
|
||||
leftToSpend: boolean;
|
||||
loanUpdate: boolean;
|
||||
summaryMonthly: boolean;
|
||||
summaryWeekly: boolean;
|
||||
transaction: boolean;
|
||||
unusualAccount: boolean;
|
||||
unusualCategory: boolean;
|
||||
};
|
||||
periodAdjustedDay: 25;
|
||||
periodMode: 'MONTHLY_ADJUSTED' | 'MONTHLY';
|
||||
timeZone: string;
|
||||
};
|
||||
// username: string; // not relevant
|
||||
} = await tinkAccountArg.request({
|
||||
urlArg: '/api/v1/user',
|
||||
accessToken,
|
||||
methodArg: 'GET',
|
||||
payloadArg: null,
|
||||
});
|
||||
const newTinkUser = new TinkUser(tinkAccountArg, responseData.id, responseData.externalUserId);
|
||||
return newTinkUser;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@ -25,29 +81,86 @@ export class TinkUser {
|
||||
public tinkUserId: string;
|
||||
public externalUserIdArg: string;
|
||||
|
||||
public authorizationToken: string
|
||||
|
||||
constructor(tinkAccountrefArg: TinkAccount, tinkUserIdArg: string, externalUserIdArg: string) {
|
||||
this.tinkAccountRef = tinkAccountrefArg;
|
||||
this.tinkUserId = tinkUserIdArg;
|
||||
this.externalUserIdArg = externalUserIdArg;
|
||||
}
|
||||
|
||||
public async authorize() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes the user
|
||||
*/
|
||||
public async delete() {
|
||||
const authorizationCode = await this.tinkAccountRef.getUserAuthorizationCode(
|
||||
this.externalUserIdArg,
|
||||
this.tinkAccountRef.clientId,
|
||||
'user:delete'
|
||||
);
|
||||
const accessToken = await this.tinkAccountRef.getUserAccessToken(authorizationCode);
|
||||
const response = await this.tinkAccountRef.request({
|
||||
externalUserId: this.externalUserIdArg,
|
||||
methodArg: 'POST',
|
||||
accessToken,
|
||||
payloadArg: {},
|
||||
scopeArg: 'user:delete',
|
||||
urlArg: '/api/v1/user/delete'
|
||||
urlArg: '/api/v1/user/delete',
|
||||
});
|
||||
console.log(response);
|
||||
console.log(`successfully deleted user with externalId ${this.externalUserIdArg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets a tink link that can be used by a user to connect accounts
|
||||
* @returns
|
||||
*/
|
||||
public async getTinkLinkForMarket(linkOptionsArg: {
|
||||
countryId: string;
|
||||
redirectUrl: string;
|
||||
/**
|
||||
* an optional state that is transported through to the callback
|
||||
*/
|
||||
customState: string;
|
||||
testProviderBool?: boolean;
|
||||
} = {
|
||||
countryId: 'NL',
|
||||
redirectUrl: 'https://console.tink.com/callback',
|
||||
customState: "exampleState",
|
||||
testProviderBool: true
|
||||
}): Promise<string> {
|
||||
if (typeof linkOptionsArg.testProviderBool !== 'boolean') {
|
||||
linkOptionsArg.testProviderBool = false;
|
||||
}
|
||||
const authorizationCode = await this.tinkAccountRef.getUserAuthorizationCode(
|
||||
this.externalUserIdArg,
|
||||
'df05e4b379934cd09963197cc855bfe9', // this is a hardcoded app id for tink link, as recommended by tink.com
|
||||
'authorization:read,authorization:grant,credentials:refresh,credentials:read,credentials:write,providers:read,user:read'
|
||||
);
|
||||
const tinkUrlOptions: {[key: string]: string} = {}
|
||||
tinkUrlOptions['client_id'] = this.tinkAccountRef.clientId;
|
||||
tinkUrlOptions['redirect_uri']= linkOptionsArg.redirectUrl;
|
||||
tinkUrlOptions['authorization_code'] = authorizationCode;
|
||||
tinkUrlOptions['market'] = linkOptionsArg.countryId;
|
||||
if (linkOptionsArg.testProviderBool) {
|
||||
tinkUrlOptions['test'] = 'true';
|
||||
}
|
||||
if (linkOptionsArg.customState) {
|
||||
tinkUrlOptions['state'] = linkOptionsArg.customState;
|
||||
}
|
||||
const url = plugins.smarturl.Smarturl.createFromUrl('https://link.tink.com/1.0/business-transactions/connect-accounts', {
|
||||
searchParams: tinkUrlOptions
|
||||
});
|
||||
const tinkLinkUrl = url.toString();
|
||||
return tinkLinkUrl;
|
||||
}
|
||||
|
||||
public async getProviderConsents(): Promise<TinkProviderConsent[]> {
|
||||
const providerConsents: TinkProviderConsent[] =
|
||||
await TinkProviderConsent.getProviderConsentsForUser(this);
|
||||
return providerConsents;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets all accounts
|
||||
*/
|
||||
public async getAllBankAccounts() {
|
||||
const bankAccounts = await BankAccount.getBankAccountsForUser(this);
|
||||
return bankAccounts;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartrequest from '@pushrocks/smartrequest';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smarturl from '@pushrocks/smarturl';
|
||||
|
||||
export {
|
||||
smartrequest
|
||||
}
|
||||
export { smartdelay, smartrequest, smartpromise, smarturl };
|
||||
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
17
tslint.json
17
tslint.json
@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user