Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/drop axios dependency #30

Merged
merged 30 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4505115
feat: SplidClient.entry.expense.create, SplidClient.entry.payment.cre…
LinusBolls Nov 4, 2024
96493ba
feat: SplidClient.entry.set, SplidClient.person.set, SplidClient.grou…
LinusBolls Nov 4, 2024
dcf8278
feat: SplidClient.file.upload method
LinusBolls Nov 4, 2024
37ac539
feat: expose parse application id and client key to the consumer
LinusBolls Nov 4, 2024
054bdd7
feat: SplidClient.getCodeConfig method
LinusBolls Nov 4, 2024
6a4f9d5
feat: SplidClient.group.create, SplidClient.person.create methods
LinusBolls Nov 5, 2024
df7c064
docs: update readme
LinusBolls Nov 5, 2024
c11ac6a
feat: SplidClient.batch method
LinusBolls Nov 5, 2024
5ae908c
feat: accept lowercase group invite codes
LinusBolls Nov 5, 2024
2ab13ee
docs: document share mode on expenses
LinusBolls Nov 5, 2024
5538ee0
Merge branch 'main' into feat/write-methods
LinusBolls Nov 6, 2024
7201942
docs: readme
LinusBolls Nov 6, 2024
35d3cb3
feat: batch updating entries, people, and group infos
LinusBolls Nov 6, 2024
0208003
feat: more intuitive api for creating expenses
LinusBolls Nov 6, 2024
ae194d1
docs: update readme usage examples to use new, optimal apis
LinusBolls Nov 6, 2024
8cfa6ff
refactor: remove unnecessary dateToIso implementation
LinusBolls Nov 6, 2024
af8b73b
fix: balance calculation being reversed 💀
LinusBolls Nov 6, 2024
ddfdc01
docs: comments for methods
LinusBolls Nov 6, 2024
e1fcb40
chore: improved types
LinusBolls Nov 6, 2024
0761ff7
chore: remove unused method
LinusBolls Nov 6, 2024
4131a24
fix: throw more descriptive error in case of network error (e.g. cors…
LinusBolls Nov 6, 2024
6c711f5
test: non-passing test case for getSuggestedPayments
LinusBolls Nov 6, 2024
6dda67c
docs: branding
LinusBolls Nov 6, 2024
eb5e1dd
devops: minor version bump
LinusBolls Nov 6, 2024
86fe176
feat: SplidClient.getBalance optionally takes currencyRates
LinusBolls Nov 7, 2024
1910470
format: import order
LinusBolls Nov 7, 2024
0f262d3
test: additional cases for SplidClient.getBalance
LinusBolls Nov 7, 2024
639bc7d
feat: SplidClient.getCurrencyRates method
LinusBolls Nov 7, 2024
0877853
chore: switch from axios to fetch api for http requests
LinusBolls Nov 7, 2024
4164357
devops: npmignore
LinusBolls Nov 7, 2024
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
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/
.prettierignore
.prettierrc.cjs
src/
docs/
*.js.map
.prettierignore
.prettierrc.cjs
1 change: 1 addition & 0 deletions .prettierrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrder: ['<THIRD_PARTY_MODULES>', '^@/(.*)$', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
Expand Down
285 changes: 240 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Splid.Js
<img src="docs/banner.svg" width="100%" />

a reverse-engineered typescript client for the Splid (https://splid.app) API.
at the moment, only read operations are supported.
# splid.js

a feature-complete typescript client for the Splid (https://splid.app) API.
Splid is a free mobile app for keeping track of expenses among friend groups.
this package is not officially associated with Splid.

last updated Nov 7 2024

## Install

Expand All @@ -18,56 +23,65 @@ npm install splid-js
import { SplidClient } from 'splid-js';

async function main() {
const client = new SplidClient();
const splid = new SplidClient();

const inviteCode = 'PWJ E2B P7A';

const groupRes = await client.group.getByInviteCode(inviteCode);
const groupRes = await splid.group.getByInviteCode(inviteCode);

const groupInfoRes = await client.groupInfo.getByGroup(
groupRes.result.objectId
);
const groupId = groupRes.result.objectId;

const entriesRes = await client.entry.getByGroup(groupRes.result.objectId);
const groupInfo = await splid.groupInfo.getOneByGroup(groupId);
const members = await splid.person.getAllByGroup(groupId);
const expensesAndPayments = await splid.entry.getAllByGroup(groupId);

const membersRes = await client.person.getByGroup(groupRes.result.objectId);

const expensesAndPayments = await client.entry.getByGroup(
groupRes.result.objectId
const balance = SplidClient.getBalance(
members,
expensesAndPayments,
groupInfo
);
const suggestedPayments = SplidClient.getSuggestedPayments(balance);

console.log(balance, suggestedPayments);
}
main();
```

```typescript
// using the returned data
// parsing the returned data
import { SplidClient } from 'splid-js';

const formatCurrency = (amount: number, currencyCode: string) => {
return (
amount.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}) + currencyCode
);
const formatCurrency = (amount: number, currency: string) => {
return amount.toLocaleString(undefined, {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
};

const getEntryDescription = (
entry: SplidJs.Entry,
members: SplidJs.Person[]
) => {
const primaryPayer = members.find((j) => j.GlobalId === entry.primaryPayer);
const primaryPayer = members.find((i) => i.GlobalId === entry.primaryPayer)!;

for (const item of entry.items) {
const totalAmount = item.AM;

const profiteers = Object.entries(item.P.P).map(([userId, share]) => {
const user = members.find((j) => j.GlobalId === userId);

const shareText = formatCurrency(totalAmount * share, entry.currencyCode);

return user.name + ' (' + shareText + ')';
});
// a map of a userId to their share (how much they profit from the expense). the shares are floats between 0 and 1 and their sum is exactly 1.
const userIdToShareMap = item.P.P;

const profiteers = Object.entries(userIdToShareMap).map(
([userId, share]) => {
const user = members.find((i) => i.GlobalId === userId)!;

const shareText = formatCurrency(
totalAmount * share,
entry.currencyCode
);
return user.name + ' (' + shareText + ')';
}
);
const profiteersText = profiteers.join(', ');

const totalText = formatCurrency(totalAmount, entry.currencyCode);
Expand All @@ -87,32 +101,213 @@ const getEntryDescription = (
};

async function main() {
const client = new SplidClient();
const splid = new SplidClient();

const inviteCode = 'PWJ E2B P7A';

const groupRes = await client.group.getByInviteCode(inviteCode);
const groupRes = await splid.group.getByInviteCode(inviteCode);

const groupInfoRes = await client.groupInfo.getByGroup(
groupRes.result.objectId
);

const entriesRes = await client.entry.getByGroup(groupRes.result.objectId);
const groupId = groupRes.result.objectId;

const membersRes = await client.person.getByGroup(groupRes.result.objectId);
const members = await splid.person.getAllByGroup(groupId);
const expensesAndPayments = await splid.entry.getAllByGroup(groupId);

for (const entry of entriesRes.result.results) {
console.log(getEntryDescription(entry, membersRes.result.results));
for (const entry of expensesAndPayments) {
console.log(getEntryDescription(entry, members));
}
}
main();
```

```typescript
// calculating members balances and suggested payments
const people = await client.person.getAllByGroup(groupId);
const entries = await client.entry.getAllByGroup(groupId);
// updating group properties
const groupInfo = await splid.groupInfo.getOneByGroup(groupId);

groupInfo.name = 'Modified Group 🔥';

groupInfo.customCategories.push('Pharmaceuticals 💊');

// setting the currency exchange rates to the most recent values
groupInfo.currencyRates = await splid.getCurrencyRates();

groupInfo.defaultCurrencyCode = 'EUR';

await splid.groupInfo.set(groupInfo);
```

```typescript
// updating group wallpaper (NodeJs)
import fs from 'fs';

const file = await fs.promises.readFile('image.png');

const uploadRes = await splid.file.upload(file);

groupInfo.wallpaperID = uploadRes.dataID;
```

```typescript
// updating person properties
const members = await splid.person.getAllByGroup(groupId);

const linus = members.find((i) => i.name === 'Linus');

linus.name = 'Alex';
linus.initials = 'A';

await splid.person.set(linus);
```

```typescript
// updating entry properties
const entries = await splid.entry.getAllByGroup(groupId);

const pizzaEntries = entries.filter((i) =>
i.title.toLowerCase().includes('pizza')
);

for (const entry of pizzaEntries) {
// setting the category of an entry
entry.category = {
type: 'custom',
originalName: 'Italian Food 🍕',
};
// setting the date of an entry
if (!entry.date) {
entry.date = {
__type: 'Date',
iso: new Date().toISOString(),
};
} else {
entry.date.iso = new Date().toISOString();
}
}
await splid.entry.set(pizzaEntries);
```

```typescript
// converting all foreign currency entries to the default currency of the group

const balance = SplidClient.getBalance(people, entries);
const suggestedPayments = SplidClient.getSuggestedPayments(balance);
const foreignCurrencyEntries = expensesAndPayments.filter(
(i) => !i.isPayment && i.currencyCode !== groupInfo.defaultCurrencyCode
);

for (const entry of foreignCurrencyEntries) {
const factor =
groupInfo.currencyRates[entry.currencyCode] /
groupInfo.currencyRates[groupInfo.defaultCurrencyCode];

entry.currencyCode = groupInfo.defaultCurrencyCode;

for (const item of entry.items) {
item.AM = item.AM * factor;
}
for (const [payerId, amount] of Object.entries(entry.secondaryPayers ?? {})) {
entry.secondaryPayers[payerId] = amount * factor;
}
}
await splid.entry.set(foreignCurrencyEntries);
```

```typescript
// deleting entries
const entries = await splid.entry.getAllByGroup(groupId);

const entry = entries[0];

entry.isDeleted = true;

await splid.entry.set(entry);
```

```typescript
// creating a group
await splid.group.create('🎉 Ramber Zamber', ['Linus', 'Laurin', 'Oskar']);
```

```typescript
// creating a basic expense
await splid.entry.expense.create(
{
groupId,
payers: [linus.GlobalId],
title: 'döner',
},
{
amount: 10,
// equivalent to equivalent to { [laurin.GlobalId]: 0.5, [oskar.GlobalId]: 0.5 }
profiteers: [laurin.GlobalId, oskar.GlobalId],
}
);
```

```typescript
// creating an expense with multiple items
await splid.entry.expense.create(
{
groupId,
payers: [linus.GlobalId],
title: 'shopping spree 😌',
},
[
{
title: 'gucci belt',
amount: 10,
profiteers: [laurin.GlobalId],
},
{
title: 'drippy hat',
amount: 15,
profiteers: [oskar.GlobalId],
},
]
);
```

```typescript
// creating an expense with multiple payers (both pay half)
await splid.entry.expense.create(
{
groupId,
// equivalent to { [linus.GlobalId]: 5, [oskar.GlobalId]: 5 }
payers: [linus.GlobalId, oskar.GlobalId],
title: 'döner',
},
{
amount: 10,
profiteers: [linus.GlobalId, oskar.GlobalId],
}
);
```

```typescript
// creating an expense with unevenly split payers (oskar pays 3€)
await splid.entry.expense.create(
{
groupId,
// equivalent to { [linus.GlobalId]: 7, [oskar.GlobalId]: 3 }
payers: [linus.GlobalId, { id: oskar.GlobalId, amount: 3 }],
title: 'shopping',
},
{
amount: 10,
profiteers: [laurin.GlobalId],
}
);
```

```typescript
// creating an expense with unevenly split profiteers (oskar owes 2.25€)
await splid.entry.expense.create(
{
groupId,
payers: [linus.GlobalId],
title: 'shopping',
},
{
amount: 10,
// equivalent to { [linus.GlobalId]: 3 / 4, [oskar.GlobalId]: 1 / 4 }
profiteers: [linus.GlobalId, { id: oskar.GlobalId, share: 1 / 4 }],
}
);
```
Loading