From a8c5a9696c1735a178158cbc9ac4f9bb4b6f013d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 8 Feb 2023 17:41:19 -0300 Subject: impl accout management and refactor --- packages/demobank-ui/src/hooks/access.ts | 330 ++++++++++++++++++++++++++++++ packages/demobank-ui/src/hooks/async.ts | 1 - packages/demobank-ui/src/hooks/backend.ts | 195 +++++++++++++++++- packages/demobank-ui/src/hooks/circuit.ts | 317 ++++++++++++++++++++++++++++ 4 files changed, 831 insertions(+), 12 deletions(-) create mode 100644 packages/demobank-ui/src/hooks/access.ts create mode 100644 packages/demobank-ui/src/hooks/circuit.ts (limited to 'packages/demobank-ui/src/hooks') 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 + */ + +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> => { + 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}`, { + 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}`, { + 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>; + 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 { + const { fetcher } = useAuthenticatedBackend(); + + const { data, error } = useSWR< + HttpResponseOk, + HttpError + >([`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 { + const { fetcher } = useAuthenticatedBackend(); + + const { data, error } = useSWR< + HttpResponseOk, + HttpError + >([`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 { + const { fetcher } = useAuthenticatedBackend(); + + const { data, error } = useSWR< + HttpResponseOk, + HttpError + >([`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 { + const { paginatedFetcher } = usePublicBackend(); + + const [page, setPage] = useState(1); + + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk, + HttpError + >([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse + >({ 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 { + const { paginatedFetcher } = useAuthenticatedBackend(); + + const [page, setPage] = useState(1); + + const { + data: afterData, + error: afterError, + isValidating: loadingAfter, + } = useSWR< + HttpResponseOk, + HttpError + >([`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], paginatedFetcher); + + const [lastAfter, setLastAfter] = useState< + HttpResponse + >({ 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( }; 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 */ +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: ( + path: string, + options?: RequestOptions, + ) => Promise>; + fetcher: (endpoint: string) => Promise>; + multiFetcher: (endpoint: string[]) => Promise[]>; + paginatedFetcher: (args: [string, number, number]) => Promise>; + sandboxAccountsFetcher: (args: [string, number, number, string]) => Promise>; +} + + +export function usePublicBackend(): useBackendType { + const { state } = useBackendContext(); + const { request: requestHandler } = useApiContext(); + + const baseUrl = state.url + + const request = useCallback( + function requestImpl( + path: string, + options: RequestOptions = {}, + ): Promise> { + + return requestHandler(baseUrl, path, options); + }, + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl(endpoint: string): Promise> { + return requestHandler(baseUrl, endpoint); + }, + [baseUrl], + ); + const paginatedFetcher = useCallback( + function fetcherImpl([endpoint, page, size]: [string, number, number]): Promise> { + return requestHandler(baseUrl, endpoint, { params: { page: page || 1, size } }); + }, + [baseUrl], + ); + const multiFetcher = useCallback( + function multiFetcherImpl( + endpoints: string[], + ): Promise[]> { + return Promise.all( + endpoints.map((endpoint) => requestHandler(baseUrl, endpoint)), + ); + }, + [baseUrl], + ); + const sandboxAccountsFetcher = useCallback( + function fetcherImpl([endpoint, page, size, account]: [string, number, number, string]): Promise> { + return requestHandler(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( + path: string, + options: RequestOptions = {}, + ): Promise> { + + return requestHandler(baseUrl, path, { basicAuth: creds, ...options }); + }, + [baseUrl, creds], + ); + + const fetcher = useCallback( + function fetcherImpl(endpoint: string): Promise> { + return requestHandler(baseUrl, endpoint, { basicAuth: creds }); + }, + [baseUrl, creds], + ); + const paginatedFetcher = useCallback( + function fetcherImpl([endpoint, page = 0, size]: [string, number, number]): Promise> { + return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page, size } }); + }, + [baseUrl, creds], + ); + const multiFetcher = useCallback( + function multiFetcherImpl( + endpoints: string[], + ): Promise[]> { + return Promise.all( + endpoints.map((endpoint) => requestHandler(baseUrl, endpoint, { basicAuth: creds })), + ); + }, + [baseUrl, creds], + ); + const sandboxAccountsFetcher = useCallback( + function fetcherImpl([endpoint, page, size, account]: [string, number, number, string]): Promise> { + return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size } }); + }, + [baseUrl], + ); + return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; +} + +export function useBackendConfig(): HttpResponse { + const { request } = usePublicBackend(); + + type Type = SandboxBackend.Config; + + const [result, setResult] = useState>({ loading: true }); + + useEffect(() => { + request(`/config`) + .then((data) => setResult(data)) + .catch((error) => setResult(error)); + }, [request]); + + return result; +} + +export function useMatchMutate(): ( + re: RegExp, + value?: unknown, +) => Promise { + 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 + */ + +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> => { + const res = await request(`circuit-api/accounts`, { + method: "POST", + data, + contentType: "json" + }); + return res; + }; + + const updateAccount = async ( + account: string, + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, + ): Promise> => { + const res = await request(`circuit-api/accounts/${account}`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + const deleteAccount = async ( + account: string, + ): Promise> => { + const res = await request(`circuit-api/accounts/${account}`, { + method: "DELETE", + contentType: "json" + }); + return res; + }; + const changePassword = async ( + account: string, + data: SandboxBackend.Circuit.AccountPasswordChange, + ): Promise> => { + const res = await request(`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> => { + const res = await request(`circuit-api/accounts/${account}`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + const changePassword = async ( + data: SandboxBackend.Circuit.AccountPasswordChange, + ): Promise> => { + const res = await request(`circuit-api/accounts/${account}/auth`, { + method: "PATCH", + data, + contentType: "json" + }); + return res; + }; + + return { updateAccount, changePassword }; +} + +export interface AdminAccountAPI { + createAccount: ( + data: SandboxBackend.Circuit.CircuitAccountRequest, + ) => Promise>; + deleteAccount: (account: string) => Promise>; + + updateAccount: ( + account: string, + data: SandboxBackend.Circuit.CircuitAccountReconfiguration + ) => Promise>; + changePassword: ( + account: string, + data: SandboxBackend.Circuit.AccountPasswordChange + ) => Promise>; +} + +export interface CircuitAccountAPI { + updateAccount: ( + data: SandboxBackend.Circuit.CircuitAccountReconfiguration + ) => Promise>; + changePassword: ( + data: SandboxBackend.Circuit.AccountPasswordChange + ) => Promise>; +} + + +export interface InstanceTemplateFilter { + //FIXME: add filter to the template list + position?: string; +} + + +export function useMyAccountDetails(): HttpResponse { + 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, + HttpError + >([`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 { + const { fetcher } = useAuthenticatedBackend(); + + const { data, error } = useSWR< + HttpResponseOk, + RequestError + >([`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 { + const { sandboxAccountsFetcher } = useAuthenticatedBackend(); + const [page, setPage] = useState(0); + + const { + data: afterData, + error: afterError, + // isValidating: loadingAfter, + } = useSWR< + HttpResponseOk, + RequestError + >([`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 + // >({ 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, + RequestError + >([`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[], + RequestError + >([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 }; +} -- cgit v1.2.3