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
91 changes: 66 additions & 25 deletions src/app/hooks/useTransactions.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,97 @@
import dayjs from "dayjs";
import { useCallback, useState } from "react";
import toast from "~/app/components/Toast";
import { useAccount } from "~/app/context/AccountContext";
import { useSettings } from "~/app/context/SettingsContext";
import api from "~/common/lib/api";
import { Transaction } from "~/types";

export const useTransactions = () => {
const { settings, getFormattedFiat } = useSettings();
const { account } = useAccount();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoadingTransactions, setIsLoadingTransactions] = useState(true);

const processTransactions = useCallback(
async (rawTransactions: any[]) => {
const processedTransactions = rawTransactions.map((transaction) => ({
...transaction,
title: transaction.memo,
timeAgo: dayjs(
transaction.settleDate || transaction.creationDate
).fromNow(),
timestamp: transaction.settleDate || transaction.creationDate,
}));

for (const transaction of processedTransactions) {
if (
transaction.displayAmount &&
transaction.displayAmount[1] === settings.currency
)
continue;
transaction.totalAmountFiat = settings.showFiat
? await getFormattedFiat(transaction.totalAmount)
: "";
}

return processedTransactions;
},
[settings, getFormattedFiat]
);

const loadTransactions = useCallback(
async (limit?: number) => {
if (!account?.id) return;

try {
const getTransactionsResponse = await api.getTransactions({
limit,
});
const transactions = getTransactionsResponse.transactions.map(
(transaction) => ({
...transaction,
title: transaction.memo,
timeAgo: dayjs(
transaction.settleDate || transaction.creationDate
).fromNow(),
timestamp: transaction.settleDate || transaction.creationDate,
})
let hasReceivedResponse = false;

// Use SWR to get cached data first, then fresh data
await api.swr.getTransactions(
account.id,
{ limit },
async (response) => {
const processedTransactions = await processTransactions(
response.transactions
);
setTransactions(processedTransactions);

// Set loading to false after we get the first response (cached or fresh)
if (!hasReceivedResponse) {
setIsLoadingTransactions(false);
hasReceivedResponse = true;
}
}
);

for (const transaction of transactions) {
if (
transaction.displayAmount &&
transaction.displayAmount[1] === settings.currency
)
continue;
transaction.totalAmountFiat = settings.showFiat
? await getFormattedFiat(transaction.totalAmount)
: "";
// If we didn't get any cached data, we still need to ensure loading is set to false
if (!hasReceivedResponse) {
setIsLoadingTransactions(false);
}

setTransactions(transactions);
setIsLoadingTransactions(false);
} catch (e) {
console.error(e);
setIsLoadingTransactions(false);
if (e instanceof Error) toast.error(`Error: ${e.message}`);
}
},
[settings, getFormattedFiat]
[account?.id, processTransactions]
);

const refreshTransactions = useCallback(
async (limit?: number) => {
if (!account?.id) return;

// Clear cache first to force fresh data
await api.clearTransactionsCache(account.id);
await loadTransactions(limit);
},
[account?.id, loadTransactions]
);

return {
transactions,
isLoadingTransactions,
loadTransactions,
refreshTransactions,
};
};
40 changes: 40 additions & 0 deletions src/common/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ import type {

import {
getAccountsCache,
getTransactionsCache,
removeAccountFromCache,
removeTransactionsFromCache,
storeAccounts,
storeTransactions,
} from "./cache";
import msg from "./msg";

Expand Down Expand Up @@ -146,6 +149,41 @@ export const swrGetAccountInfo = async (
.catch(reject);
});
};

/**
* stale-while-revalidate get transactions
* @param accountId - account id
* @param options - transaction options (limit, etc.)
* @param callback - will be called first with cached (stale) data first, then with fresh data.
*/
export const swrGetTransactions = async (
accountId: string,
options?: { limit?: number },
callback?: (transactions: { transactions: Invoice[] }) => void
): Promise<{ transactions: Invoice[] }> => {
const cachedTransactions = await getTransactionsCache(accountId);

return new Promise((resolve, reject) => {
if (cachedTransactions) {
const cachedResponse = { transactions: cachedTransactions };
if (callback) callback(cachedResponse);
resolve(cachedResponse);
}

// Update transactions with most recent data, save to cache.
getTransactions(options)
.then((response) => {
storeTransactions(accountId, response.transactions);
if (callback) callback(response);
return resolve(response);
})
.catch(reject);
});
};
export const clearTransactionsCache = async (accountId: string) => {
await removeTransactionsFromCache(accountId);
};

export const getAccounts = () => msg.request<Accounts>("getAccounts");
export const getAccount = (id?: string) =>
msg.request<GetAccountRes>("getAccount", {
Expand Down Expand Up @@ -313,7 +351,9 @@ export default {
setSetting,
swr: {
getAccountInfo: swrGetAccountInfo,
getTransactions: swrGetTransactions,
},
clearTransactionsCache,
removeAccount,
unlock,
getBlocklist,
Expand Down
30 changes: 29 additions & 1 deletion src/common/lib/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import browser from "webextension-polyfill";
import type { AccountInfo } from "~/types";
import type { AccountInfo, Invoice } from "~/types";

export const getAccountsCache = async () => {
let accountsCache: { [id: string]: AccountInfo } = {};
Expand All @@ -24,3 +24,31 @@ export const removeAccountFromCache = async (id: string) => {
}
return accountsCache;
};

export const getTransactionsCache = async (accountId: string) => {
const cacheKey = `transactions_${accountId}`;
const result = await browser.storage.local.get([cacheKey]);
if (result[cacheKey]) {
const cached = JSON.parse(result[cacheKey]);
// Check if cache is less than 5 minutes old
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.transactions;
}
}
return null;
};

export const storeTransactions = (accountId: string, transactions: Invoice[]) => {
const cacheKey = `transactions_${accountId}`;
browser.storage.local.set({
[cacheKey]: JSON.stringify({
transactions,
timestamp: Date.now(),
}),
});
};

export const removeTransactionsFromCache = async (accountId: string) => {
const cacheKey = `transactions_${accountId}`;
await browser.storage.local.remove([cacheKey]);
};