/* 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 */ import { HttpResponse, HttpResponseOk, HttpResponsePaginated, RequestError, } from "@gnu-taler/web-util/lib/index.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 > => { const res = await request( `access-api/accounts/${account}/withdrawals`, { method: "POST", data, contentType: "json", }, ); return res; }; const abortWithdrawal = async (id: string): Promise> => { const res = await request( `access-api/accounts/${account}/withdrawals/${id}/abort`, { method: "POST", contentType: "json", }, ); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); return res; }; const confirmWithdrawal = async ( id: string, ): Promise> => { const res = await request( `access-api/accounts/${account}/withdrawals/${id}/confirm`, { method: "POST", contentType: "json", }, ); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); return res; }; const createTransaction = async ( data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ): Promise> => { const res = await request( `access-api/accounts/${account}/transactions`, { method: "POST", data, contentType: "json", }, ); await mutateAll(/.*accounts\/.*\/transactions.*/); return res; }; const deleteAccount = async (): Promise> => { const res = await request(`access-api/accounts/${account}`, { method: "DELETE", contentType: "json", }); await mutateAll(/.*accounts\/.*/); return res; }; return { abortWithdrawal, confirmWithdrawal, createWithdrawal, createTransaction, deleteAccount, }; } export function useTestingAPI(): TestingAPI { const mutateAll = useMatchMutate(); const { request: noAuthRequest } = usePublicBackend(); const register = async ( data: SandboxBackend.Access.BankRegistrationRequest, ): Promise> => { const res = await noAuthRequest(`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>; } export interface AccessAPI { createWithdrawal: ( data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, ) => Promise< HttpResponseOk >; abortWithdrawal: (wid: string) => Promise>; confirmWithdrawal: (wid: string) => Promise>; createTransaction: ( data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ) => Promise>; deleteAccount: () => Promise>; } 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, RequestError >([`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 = "100"; } //FIXME: sandbox server should return amount string if (data) { const d = structuredClone(data); const { currency } = Amounts.parseOrThrow(data.data.balance.amount); d.data.debitThreshold = Amounts.stringify({ currency, value: Number.parseInt(d.data.debitThreshold, 10), fraction: 0, }); return d; } if (error) return error.info; return { loading: true }; } // FIXME: should poll export function useWithdrawalDetails( account: string, wid: string, ): HttpResponse< SandboxBackend.Access.BankAccountGetWithdrawalResponse, SandboxBackend.SandboxError > { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk, RequestError >([`access-api/accounts/${account}/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.info; return { loading: true }; } export function useTransactionDetails( account: string, tid: string, ): HttpResponse< SandboxBackend.Access.BankAccountTransactionInfo, SandboxBackend.SandboxError > { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk, RequestError >([`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.info; 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, RequestError >([`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.info; // 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, RequestError >( [`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.info; // 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 }; }