diff options
Diffstat (limited to 'packages/demobank-ui/src/hooks')
-rw-r--r-- | packages/demobank-ui/src/hooks/access.ts | 330 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/async.ts | 1 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/backend.ts | 195 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/circuit.ts | 317 |
4 files changed, 831 insertions, 12 deletions
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts new file mode 100644 index 000000000..4d4574dac --- /dev/null +++ b/packages/demobank-ui/src/hooks/access.ts @@ -0,0 +1,330 @@ +/* + 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 useSWR from "swr"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; +import { useEffect, useState } from "preact/hooks"; +import { + HttpError, + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, +} from "@gnu-taler/web-util/lib/index.browser"; +import { useAuthenticatedBackend, useMatchMutate, usePublicBackend } from "./backend.js"; +import { useBackendContext } from "../context/backend.js"; + +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 abortWithdrawal = async ( + id: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { + method: "POST", + contentType: "json" + }); + await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); + return res; + }; + const confirmWithdrawal = async ( + id: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { + method: "POST", + contentType: "json" + }); + await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); + 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 { abortWithdrawal, confirmWithdrawal, createWithdrawal, createTransaction, deleteAccount }; +} + +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>>; + abortWithdrawal: ( + wid: string, + ) => Promise<HttpResponseOk<void>>; + confirmWithdrawal: ( + wid: string + ) => Promise<HttpResponseOk<void>>; + createTransaction: ( + data: SandboxBackend.Access.CreateBankAccountTransactionCreate + ) => Promise<HttpResponseOk<void>>; + deleteAccount: () => 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>, + HttpError<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, + }); + + if (data) return data; + if (error) return error; + 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<SandboxBackend.Access.BankAccountGetWithdrawalResponse>, + HttpError<SandboxBackend.SandboxError> + >([`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; + 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>, + HttpError<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; + 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>, + HttpError<SandboxBackend.SandboxError> + >([`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; + + // 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>, + HttpError<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; + + // 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 }; +} diff --git a/packages/demobank-ui/src/hooks/async.ts b/packages/demobank-ui/src/hooks/async.ts index 6492b7729..b968cfb84 100644 --- a/packages/demobank-ui/src/hooks/async.ts +++ b/packages/demobank-ui/src/hooks/async.ts @@ -62,7 +62,6 @@ export function useAsync<T>( }; function cancel() { - // cancelPendingRequest() setLoading(false); setSlow(false); } diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 13a158f4f..f4f5ecfd0 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -14,7 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser"; +import { + HttpResponse, + HttpResponseOk, + RequestOptions, +} from "@gnu-taler/web-util/lib/index.browser"; +import { useApiContext } from "@gnu-taler/web-util/lib/index.browser"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useSWRConfig } from "swr"; +import { useBackendContext } from "../context/backend.js"; /** * Has the information to reach and @@ -22,25 +32,38 @@ import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser"; */ export type BackendState = LoggedIn | LoggedOut; -export interface BackendInfo { - url: string; +export interface BackendCredentials { username: string; password: string; } -interface LoggedIn extends BackendInfo { +interface LoggedIn extends BackendCredentials { + url: string; status: "loggedIn"; + isUserAdministrator: boolean; } interface LoggedOut { + url: string; status: "loggedOut"; } -export const defaultState: BackendState = { status: "loggedOut" }; +const maybeRootPath = "https://bank.demo.taler.net/demobanks/default/"; + +export function getInitialBackendBaseURL(): string { + const overrideUrl = localStorage.getItem("bank-base-url"); + + return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath); +} + +export const defaultState: BackendState = { + status: "loggedOut", + url: getInitialBackendBaseURL() +}; export interface BackendStateHandler { state: BackendState; - clear(): void; - save(info: BackendInfo): void; + logOut(): void; + logIn(info: BackendCredentials): void; } /** * Return getters and setters for @@ -52,7 +75,7 @@ export function useBackendState(): BackendStateHandler { "backend-state", JSON.stringify(defaultState), ); - // const parsed = value !== undefined ? JSON.parse(value) : value; + let parsed; try { parsed = JSON.parse(value!); @@ -63,12 +86,162 @@ export function useBackendState(): BackendStateHandler { return { state, - clear() { - update(JSON.stringify(defaultState)); + logOut() { + update(JSON.stringify({ ...defaultState, url: state.url })); }, - save(info) { - const nextState: BackendState = { status: "loggedIn", ...info }; + logIn(info) { + //admin is defined by the username + const nextState: BackendState = { status: "loggedIn", url: state.url, ...info, isUserAdministrator: info.username === "admin" }; update(JSON.stringify(nextState)); }, }; } + +interface useBackendType { + request: <T>( + path: string, + options?: RequestOptions, + ) => Promise<HttpResponseOk<T>>; + fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>[]>; + paginatedFetcher: <T>(args: [string, number, number]) => Promise<HttpResponseOk<T>>; + sandboxAccountsFetcher: <T>(args: [string, number, number, string]) => Promise<HttpResponseOk<T>>; +} + + +export function usePublicBackend(): useBackendType { + const { state } = useBackendContext(); + const { request: requestHandler } = useApiContext(); + + const baseUrl = state.url + + const request = useCallback( + function requestImpl<T>( + path: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + + return requestHandler<T>(baseUrl, path, options); + }, + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint); + }, + [baseUrl], + ); + const paginatedFetcher = useCallback( + function fetcherImpl<T>([endpoint, page, size]: [string, number, number]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); + }, + [baseUrl], + ); + const multiFetcher = useCallback( + function multiFetcherImpl<T>( + endpoints: string[], + ): Promise<HttpResponseOk<T>[]> { + return Promise.all( + endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)), + ); + }, + [baseUrl], + ); + const sandboxAccountsFetcher = useCallback( + function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); + }, + [baseUrl], + ); + return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; +} + +export function useAuthenticatedBackend(): useBackendType { + const { state } = useBackendContext(); + const { request: requestHandler } = useApiContext(); + + const creds = state.status === "loggedIn" ? state : undefined + const baseUrl = state.url + + const request = useCallback( + function requestImpl<T>( + path: string, + options: RequestOptions = {}, + ): Promise<HttpResponseOk<T>> { + + return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options }); + }, + [baseUrl, creds], + ); + + const fetcher = useCallback( + function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }); + }, + [baseUrl, creds], + ); + const paginatedFetcher = useCallback( + function fetcherImpl<T>([endpoint, page = 0, size]: [string, number, number]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page, size } }); + }, + [baseUrl, creds], + ); + const multiFetcher = useCallback( + function multiFetcherImpl<T>( + endpoints: string[], + ): Promise<HttpResponseOk<T>[]> { + return Promise.all( + endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { basicAuth: creds })), + ); + }, + [baseUrl, creds], + ); + const sandboxAccountsFetcher = useCallback( + function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size } }); + }, + [baseUrl], + ); + return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; +} + +export function useBackendConfig(): HttpResponse<SandboxBackend.Config, SandboxBackend.SandboxError> { + const { request } = usePublicBackend(); + + type Type = SandboxBackend.Config; + + const [result, setResult] = useState<HttpResponse<Type, SandboxBackend.SandboxError>>({ loading: true }); + + useEffect(() => { + request<Type>(`/config`) + .then((data) => setResult(data)) + .catch((error) => setResult(error)); + }, [request]); + + return result; +} + +export function useMatchMutate(): ( + re: RegExp, + value?: unknown, +) => Promise<any> { + const { cache, mutate } = useSWRConfig(); + + if (!(cache instanceof Map)) { + throw new Error( + "matchMutate requires the cache provider to be a Map instance", + ); + } + + return function matchRegexMutate(re: RegExp, value?: unknown) { + const allKeys = Array.from(cache.keys()); + const keys = allKeys.filter((key) => re.test(key)); + const mutations = keys.map((key) => { + mutate(key, value, true); + }); + return Promise.all(mutations); + }; +} + + diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts new file mode 100644 index 000000000..6e9ada601 --- /dev/null +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -0,0 +1,317 @@ +/* + 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 { + HttpError, + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError +} from "@gnu-taler/web-util/lib/index.browser"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import useSWR from "swr"; +import { useBackendContext } from "../context/backend.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; +import { useAuthenticatedBackend } from "./backend.js"; + +export function useAdminAccountAPI(): AdminAccountAPI { + 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 createAccount = async ( + data: SandboxBackend.Circuit.CircuitAccountRequest, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts`, { + method: "POST", + data, + contentType: "json" + }); + return res; + }; + + const updateAccount = async ( + account: string, + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts/${account}`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + const deleteAccount = async ( + account: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts/${account}`, { + method: "DELETE", + contentType: "json" + }); + return res; + }; + const changePassword = async ( + account: string, + data: SandboxBackend.Circuit.AccountPasswordChange, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts/${account}/auth`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + + return { createAccount, deleteAccount, updateAccount, changePassword }; +} + +export function useCircuitAccountAPI(): CircuitAccountAPI { + 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 updateAccount = async ( + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts/${account}`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + const changePassword = async ( + data: SandboxBackend.Circuit.AccountPasswordChange, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`circuit-api/accounts/${account}/auth`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + + return { updateAccount, changePassword }; +} + +export interface AdminAccountAPI { + createAccount: ( + data: SandboxBackend.Circuit.CircuitAccountRequest, + ) => Promise<HttpResponseOk<void>>; + deleteAccount: (account: string) => Promise<HttpResponseOk<void>>; + + updateAccount: ( + account: string, + data: SandboxBackend.Circuit.CircuitAccountReconfiguration + ) => Promise<HttpResponseOk<void>>; + changePassword: ( + account: string, + data: SandboxBackend.Circuit.AccountPasswordChange + ) => Promise<HttpResponseOk<void>>; +} + +export interface CircuitAccountAPI { + updateAccount: ( + data: SandboxBackend.Circuit.CircuitAccountReconfiguration + ) => Promise<HttpResponseOk<void>>; + changePassword: ( + data: SandboxBackend.Circuit.AccountPasswordChange + ) => Promise<HttpResponseOk<void>>; +} + + +export interface InstanceTemplateFilter { + //FIXME: add filter to the template list + position?: string; +} + + +export function useMyAccountDetails(): HttpResponse<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> { + const { fetcher } = useAuthenticatedBackend(); + const { state } = useBackendContext() + if (state.status === "loggedOut") { + throw Error("can't access my-account-details when logged out") + } + const { data, error } = useSWR< + HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>, + HttpError<SandboxBackend.SandboxError> + >([`accounts/${state.username}`], 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 { loading: true }; +} + +export function useAccountDetails(account: string): HttpResponse<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> { + const { fetcher } = useAuthenticatedBackend(); + + const { data, error } = useSWR< + HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>, + RequestError<SandboxBackend.SandboxError> + >([`circuit-api/accounts/${account}`], 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 { + account?: string, + page?: number, +} + +export function useAccounts( + args?: PaginationFilter, +): HttpResponsePaginated<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> { + const { sandboxAccountsFetcher } = useAuthenticatedBackend(); + const [page, setPage] = useState(0); + + const { + data: afterData, + error: afterError, + // isValidating: loadingAfter, + } = useSWR< + HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, + RequestError<SandboxBackend.SandboxError> + >([`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], sandboxAccountsFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + keepPreviousData: true, + }); + + // const [lastAfter, setLastAfter] = useState< + // HttpResponse<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> + // >({ loading: true }); + + // useEffect(() => { + // if (afterData) setLastAfter(afterData); + // }, [afterData]); + + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = + afterData && afterData.data?.customers?.length < PAGE_SIZE; + const isReachingStart = false; + + const pagination = { + isReachingEnd, + isReachingStart, + loadMore: () => { + if (!afterData || isReachingEnd) return; + if (afterData.data?.customers?.length < MAX_RESULT_SIZE) { + setPage(page + 1); + } + }, + loadMorePrev: () => { + null + }, + }; + + const result = useMemo(() => { + const customers = !afterData ? [] : (afterData)?.data?.customers ?? []; + return { ok: true as const, data: { customers }, ...pagination } + }, [afterData?.data]) + + if (afterError) return afterError.info; + if (afterData) { + return result + } + + // if (loadingAfter) + // return { loading: true, data: { customers } }; + // if (afterData) { + // return { ok: true, data: { customers }, ...pagination }; + // } + return { loading: true }; +} + +export function useCashouts(): HttpResponse< + (SandboxBackend.Circuit.CashoutStatusResponse & WithId)[], + SandboxBackend.SandboxError +> { + const { fetcher, multiFetcher } = useAuthenticatedBackend(); + + const { data: list, error: listError } = useSWR< + HttpResponseOk<SandboxBackend.Circuit.Cashouts>, + RequestError<SandboxBackend.SandboxError> + >([`circuit-api/cashouts`], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + const paths = (list?.data.cashouts || []).map( + (cashoutId) => `circuit-api/cashouts/${cashoutId}`, + ); + const { data: cashouts, error: productError } = useSWR< + HttpResponseOk<SandboxBackend.Circuit.CashoutStatusResponse>[], + RequestError<SandboxBackend.SandboxError> + >([paths], multiFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.info; + if (productError) return productError.info; + + if (cashouts) { + const dataWithId = cashouts.map((d) => { + //take the id from the queried url + return { + ...d.data, + id: d.info?.url.replace(/.*\/cashouts\//, "") || "", + }; + }); + return { ok: true, data: dataWithId }; + } + return { loading: true }; +} |