summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/hooks/regional.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/hooks/regional.ts')
-rw-r--r--packages/bank-ui/src/hooks/regional.ts484
1 files changed, 484 insertions, 0 deletions
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
new file mode 100644
index 000000000..bf948d293
--- /dev/null
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -0,0 +1,484 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { PAGE_SIZE } from "../utils.js";
+import { useSessionState } from "./session.js";
+
+import {
+ AccessToken,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ OperationOk,
+ TalerBankConversionResultByMethod,
+ TalerCoreBankErrorsByMethod,
+ TalerCoreBankResultByMethod,
+ TalerCorebankApi,
+ TalerError,
+ TalerHttpError,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useState } from "preact/hooks";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+const useSWR = _useSWR as unknown as SWRHook;
+
+export type TransferCalculation = {
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+} | "amount-is-too-small";
+type EstimatorFunction = (
+ amount: AmountJson,
+ fee: AmountJson,
+) => Promise<TransferCalculation>;
+
+type ConversionEstimators = {
+ estimateByCredit: EstimatorFunction;
+ estimateByDebit: EstimatorFunction;
+};
+
+export function revalidateConversionInfo() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
+ );
+}
+export function useConversionInfo() {
+ const { conversion, config } = useBankCoreApiContext();
+
+ async function fetcher() {
+ return await conversion.getConfig();
+ }
+ const { data, error } = useSWR<
+ TalerBankConversionResultByMethod<"getConfig">,
+ TalerHttpError
+ >(!config.allow_conversion ? undefined : ["getConversionInfoAPI"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function useCashinEstimator(): ConversionEstimators {
+ const { conversion } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small"
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small"
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+export function useCashoutEstimator(): ConversionEstimators {
+ const { bank, conversion } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small"
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small"
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+/**
+ * @deprecated use useCashoutEstimator
+ */
+export function useEstimator(): ConversionEstimators {
+ return useCashoutEstimator()
+}
+
+export async function revalidateBusinessAccounts() {
+ return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true });
+}
+export function useBusinessAccounts() {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const { bank: api } = useBankCoreApiContext();
+
+ const [offset, setOffset] = useState<number | undefined>();
+
+ function fetcher([token, offset]: [AccessToken, number]) {
+ // FIXME: add account name filter
+ return api.getAccounts(
+ token,
+ {},
+ {
+ limit: PAGE_SIZE + 1,
+ offset: String(offset),
+ order: "asc",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getAccounts">,
+ TalerHttpError
+ >([token, offset ?? 0, "getAccounts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ const isLastPage =
+ data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE;
+ const isFirstPage = !offset;
+
+ const result = data && data.type == "ok" ? structuredClone(data.body.accounts) : []
+ if (result.length == PAGE_SIZE + 1) {
+ result.pop()
+ }
+ const pagination = {
+ result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ setOffset(result[result.length - 1].row_id);
+ },
+ loadFirst: () => {
+ setOffset(0);
+ },
+ };
+
+ if (data) return { ok: true, data, ...pagination };
+ if (error) return error;
+ return undefined;
+}
+
+type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number };
+function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
+ return c !== undefined;
+}
+export function revalidateOnePendingCashouts() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", undefined, { revalidate: true }
+ );
+}
+export function useOnePendingCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const { bank: api, config } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const pendingCashout = list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
+ if (!pendingCashout) return opFixedSuccess(undefined);
+ const cashoutInfo = await api.getCashoutById(
+ { username, token },
+ pendingCashout.cashout_id,
+ );
+ if (cashoutInfo.type !== "ok") {
+ return cashoutInfo;
+ }
+ return opFixedSuccess({
+ ...cashoutInfo.body,
+ id: pendingCashout.cashout_id,
+ });
+ }
+
+ const { data, error } = useSWR<
+ | OperationOk<CashoutWithId | undefined>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">
+ | TalerCoreBankErrorsByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ !config.allow_conversion
+ ? undefined
+ : [account, token, "useOnePendingCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashouts() {
+ return mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts");
+}
+export function useCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const { bank: api, config } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const all: Array<CashoutWithId | undefined> = await Promise.all(
+ list.body.cashouts.map(async (c) => {
+ const r = await api.getCashoutById({ username, token }, c.cashout_id);
+ if (r.type === "fail") {
+ return undefined;
+ }
+ return { ...r.body, id: c.cashout_id };
+ }),
+ );
+ const cashouts = all.filter(notUndefined);
+ return { type: "ok" as const, body: { cashouts }};
+ }
+ const { data, error } = useSWR<
+ | OperationOk<{ cashouts: CashoutWithId[] }>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">,
+ TalerHttpError
+ >(
+ !config.allow_conversion ? undefined : [account, token, "useCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashoutDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", undefined, { revalidate: true }
+ );
+}
+export function useCashoutDetails(cashoutId: number | undefined) {
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const { bank: api } = useBankCoreApiContext();
+
+ async function fetcher([username, token, id]: [string, AccessToken, number]) {
+ return api.getCashoutById({ username, token }, id);
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ cashoutId === undefined
+ ? undefined
+ : [creds?.username, creds?.token, cashoutId, "getCashoutById"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+export type MonitorMetrics = {
+ lastHour: TalerCoreBankResultByMethod<"getMonitor">;
+ lastDay: TalerCoreBankResultByMethod<"getMonitor">;
+ lastMonth: TalerCoreBankResultByMethod<"getMonitor">;
+};
+
+export type LastMonitor = {
+ current: TalerCoreBankResultByMethod<"getMonitor">;
+ previous: TalerCoreBankResultByMethod<"getMonitor">;
+};
+export function revalidateLastMonitorInfo() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", undefined, { revalidate: true }
+ );
+}
+export function useLastMonitorInfo(
+ currentMoment: number,
+ previousMoment: number,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+) {
+ const { bank: api } = useBankCoreApiContext();
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([token, timeframe]: [
+ AccessToken,
+ TalerCorebankApi.MonitorTimeframeParam,
+ ]) {
+ const [current, previous] = await Promise.all([
+ api.getMonitor(token, { timeframe, which: currentMoment }),
+ api.getMonitor(token, { timeframe, which: previousMoment }),
+ ]);
+ return {
+ current,
+ previous,
+ };
+ }
+
+ const { data, error } = useSWR<LastMonitor, TalerHttpError>(
+ !token ? undefined : [token, timeframe, "useLastMonitorInfo"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}