wallet-core/packages/demobank-ui/src/hooks/access.ts

413 lines
12 KiB
TypeScript

/*
This file is part of GNU Taler
(C) 2022 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 {
HttpResponse,
HttpResponseOk,
HttpResponsePaginated,
RequestError,
} from "@gnu-taler/web-util/browser";
import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
import {
useAuthenticatedBackend,
useMatchMutate,
usePublicBackend,
} from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
import { Amounts } from "@gnu-taler/taler-util";
const useSWR = _useSWR as unknown as SWRHook;
export function useAccessAPI(): AccessAPI {
const mutateAll = useMatchMutate();
const { request } = useAuthenticatedBackend();
const { state } = useBackendContext();
if (state.status === "loggedOut") {
throw Error("access-api can't be used when the user is not logged In");
}
const account = state.username;
const createWithdrawal = async (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
): Promise<
HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
> => {
const res =
await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(
`access-api/accounts/${account}/withdrawals`,
{
method: "POST",
data,
contentType: "json",
},
);
return res;
};
const createTransaction = async (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(
`access-api/accounts/${account}/transactions`,
{
method: "POST",
data,
contentType: "json",
},
);
await mutateAll(/.*accounts\/.*\/transactions.*/);
return res;
};
const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/accounts/${account}`, {
method: "DELETE",
contentType: "json",
});
await mutateAll(/.*accounts\/.*/);
return res;
};
return {
createWithdrawal,
createTransaction,
deleteAccount,
};
}
export function useAccessAnonAPI(): AccessAnonAPI {
const mutateAll = useMatchMutate();
const { request } = useAuthenticatedBackend();
const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/withdrawals/${id}/abort`, {
method: "POST",
contentType: "json",
});
await mutateAll(/.*withdrawals\/.*/);
return res;
};
const confirmWithdrawal = async (
id: string,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/withdrawals/${id}/confirm`, {
method: "POST",
contentType: "json",
});
await mutateAll(/.*withdrawals\/.*/);
return res;
};
return {
abortWithdrawal,
confirmWithdrawal,
};
}
export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend();
const register = async (
data: SandboxBackend.Access.BankRegistrationRequest,
): Promise<HttpResponseOk<void>> => {
const res = await noAuthRequest<void>(`access-api/testing/register`, {
method: "POST",
data,
contentType: "json",
});
await mutateAll(/.*accounts\/.*/);
return res;
};
return { register };
}
export interface TestingAPI {
register: (
data: SandboxBackend.Access.BankRegistrationRequest,
) => Promise<HttpResponseOk<void>>;
}
export interface AccessAPI {
createWithdrawal: (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
) => Promise<
HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
>;
createTransaction: (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
) => Promise<HttpResponseOk<void>>;
deleteAccount: () => Promise<HttpResponseOk<void>>;
}
export interface AccessAnonAPI {
abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
}
export interface InstanceTemplateFilter {
//FIXME: add filter to the template list
position?: string;
}
export function useAccountDetails(
account: string,
): HttpResponse<
SandboxBackend.Access.BankAccountBalanceResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
//FIXME: remove optional when libeufin sandbox has implemented the feature
if (data && typeof data.data.debitThreshold === "undefined") {
data.data.debitThreshold = "0";
}
//FIXME: sandbox server should return amount string
if (data) {
const isAmount = Amounts.parse(data.data.debitThreshold);
if (isAmount) {
//server response with correct format
return data;
}
const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
const clone = structuredClone(data);
const theNumber = Number.parseInt(data.data.debitThreshold, 10);
const value = Number.isNaN(theNumber) ? 0 : theNumber;
clone.data.debitThreshold = Amounts.stringify({
currency,
value: value,
fraction: 0,
});
return clone;
}
if (error) return error.cause;
return { loading: true };
}
// FIXME: should poll
export function useWithdrawalDetails(
wid: string,
): HttpResponse<
SandboxBackend.Access.BankAccountGetWithdrawalResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/withdrawals/${wid}`], fetcher, {
refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
// if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error.cause;
return { loading: true };
}
export function useTransactionDetails(
account: string,
tid: string,
): HttpResponse<
SandboxBackend.Access.BankAccountTransactionInfo,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
});
// if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error.cause;
return { loading: true };
}
interface PaginationFilter {
page: number;
}
export function usePublicAccounts(
args?: PaginationFilter,
): HttpResponsePaginated<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend();
const [page, setPage] = useState(1);
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
useEffect(() => {
if (afterData) setLastAfter(afterData);
}, [afterData]);
if (afterError) return afterError.cause;
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
loadMorePrev: () => {
null;
},
};
const publicAccounts = !afterData
? []
: (afterData || lastAfter).data.publicAccounts;
if (loadingAfter) return { loading: true, data: { publicAccounts } };
if (afterData) {
return { ok: true, data: { publicAccounts }, ...pagination };
}
return { loading: true };
}
/**
* FIXME: mutate result when balance change (transaction )
* @param account
* @param args
* @returns
*/
export function useTransactions(
account: string,
args?: PaginationFilter,
): HttpResponsePaginated<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend();
const [page, setPage] = useState(1);
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
RequestError<SandboxBackend.SandboxError>
>(
[`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
paginatedFetcher,
);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
useEffect(() => {
if (afterData) setLastAfter(afterData);
}, [afterData]);
if (afterError) {
return afterError.cause;
}
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.transactions.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
loadMorePrev: () => {
null;
},
};
const transactions = !afterData
? []
: (afterData || lastAfter).data.transactions;
if (loadingAfter) return { loading: true, data: { transactions } };
if (afterData) {
return { ok: true, data: { transactions }, ...pagination };
}
return { loading: true };
}