diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-03 09:52:53 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-03 14:56:29 -0300 |
commit | 56da180423029a1b53d2be343eed4f073e96dc89 (patch) | |
tree | e8fd31dcc4c7bc6866b139de097c8f2c01f93597 /packages | |
parent | c53c6b8b3c0a66f3862883ec1314c6d4bf68af32 (diff) | |
download | wallet-core-56da180423029a1b53d2be343eed4f073e96dc89.tar.xz |
wip #8655: updating paginated queries
Diffstat (limited to 'packages')
20 files changed, 537 insertions, 1836 deletions
diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts index 4e4552a82..ce6338e57 100644 --- a/packages/bank-ui/src/components/Transactions/state.ts +++ b/packages/bank-ui/src/components/Transactions/state.ts @@ -17,7 +17,9 @@ import { AbsoluteTime, Amounts, + HttpStatusCode, TalerError, + assertUnreachable, parsePaytoUri, } from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/account.js"; @@ -27,21 +29,27 @@ export function useComponentState({ account, routeCreateWireTransfer, }: Props): State { - const txResult = useTransactions(account); - if (!txResult) { + const result = useTransactions(account); + if (!result) { return { status: "loading", error: undefined, }; } - if (txResult instanceof TalerError) { + if (result instanceof TalerError) { return { status: "loading-error", - error: txResult, + error: result, + }; + } + if (result.type === "fail") { + return { + status: "loading", + error: undefined, }; } - const transactions = txResult.result + const transactions = result.body .map((tx) => { const negative = tx.direction === "debit"; const cp = parsePaytoUri( @@ -76,7 +84,7 @@ export function useComponentState({ error: undefined, routeCreateWireTransfer, transactions, - onGoNext: txResult.isLastPage ? undefined : txResult.loadNext, - onGoStart: txResult.isFirstPage ? undefined : txResult.loadFirst, + onGoNext: result.isLastPage ? undefined : result.loadNext, + onGoStart: result.isFirstPage ? undefined : result.loadFirst, }; } diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts index 24309183f..543c49aed 100644 --- a/packages/bank-ui/src/hooks/account.ts +++ b/packages/bank-ui/src/hooks/account.ts @@ -16,6 +16,7 @@ import { AccessToken, + OperationOk, TalerCoreBankResultByMethod, TalerHttpError, WithdrawalOperationStatus, @@ -197,36 +198,44 @@ export function usePublicAccounts( keepPreviousData: true, }); - const isLastPage = - data && data.type === "ok" && data.body.public_accounts.length <= PAGE_SIZE; - const isFirstPage = !offset; + if (error) return error; + if (data === undefined) return undefined; + // if (data.type !== "ok") return data; + + //TODO: row_id should not be optional + return buildPaginatedResult(data.body.public_accounts, offset, setOffset, (d) => d.row_id ?? 0) +} + - const result = - data && data.type == "ok" ? structuredClone(data.body.public_accounts) : []; +type PaginatedResult<T> = OperationOk<T> & { + isLastPage: boolean; + isFirstPage: boolean; + loadNext(): void; + loadFirst(): void; +} +//TODO: consider sending this to web-util +export function buildPaginatedResult<DataType, OffsetId>(data: DataType[], offset: OffsetId | undefined, setOffset: (o: OffsetId | undefined) => void, getId: (r: DataType) => OffsetId): PaginatedResult<DataType[]> { + const isLastPage = data.length <= PAGE_SIZE; + const isFirstPage = offset === undefined; + + const result = structuredClone(data); if (result.length == PAGE_SIZE + 1) { result.pop(); } - const pagination = { - result, + return { + type: "ok", + body: result, isLastPage, isFirstPage, loadNext: () => { if (!result.length) return; - setOffset(result[result.length - 1].row_id); + const id = getId(result[result.length - 1]) + setOffset(id); }, loadFirst: () => { - setOffset(0); + setOffset(undefined); }, }; - - // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts; - if (data) { - return { ok: true, data: data.body, ...pagination }; - } - if (error) { - return error; - } - return undefined; } export function revalidateTransactions() { @@ -271,34 +280,10 @@ export function useTransactions(account: string, initial?: number) { revalidateOnFocus: false, revalidateOnReconnect: false, }); + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const isLastPage = - data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE; - const isFirstPage = !offset; - - const result = - data && data.type == "ok" ? structuredClone(data.body.transactions) : []; - 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); - }, - }; + return buildPaginatedResult(data.body.transactions, offset, setOffset, (d) => d.row_id) - if (data) { - return { ok: true, data, ...pagination }; - } - if (error) { - return error; - } - return undefined; } diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts index 274638f74..3e832a944 100644 --- a/packages/bank-ui/src/hooks/regional.ts +++ b/packages/bank-ui/src/hooks/regional.ts @@ -34,6 +34,7 @@ import { import { useState } from "preact/hooks"; import _useSWR, { SWRHook, mutate } from "swr"; import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; +import { buildPaginatedResult } from "./account.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 const useSWR = _useSWR as unknown as SWRHook; @@ -249,31 +250,13 @@ export function useBusinessAccounts() { keepPreviousData: true, }); - const isLastPage = - data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE; - const isFirstPage = !offset; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - 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); - }, - }; + //TODO: row_id should not be optional + return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.row_id ?? 0) - if (data) return { ok: true, data, ...pagination }; - if (error) return error; - return undefined; } type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number }; diff --git a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx index 554da0c3f..1810bd5dd 100644 --- a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx +++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx @@ -31,9 +31,9 @@ export function PublicHistoriesPage(): VNode { const result = usePublicAccounts(undefined); const firstAccount = result && - !(result instanceof TalerError) && - result.data.public_accounts.length > 0 - ? result.data.public_accounts[0].username + !(result instanceof TalerError) && + result.body.length > 0 + ? result.body[0].username : undefined; const [showAccount, setShowAccount] = useState(firstAccount); @@ -45,13 +45,13 @@ export function PublicHistoriesPage(): VNode { return <Loading />; } - const { data } = result; + const { body: accountList } = result; const txs: Record<string, h.JSX.Element> = {}; const accountsBar = []; // Ask story of all the public accounts. - for (const account of data.public_accounts) { + for (const account of accountList) { const isSelected = account.username == showAccount; accountsBar.push( <li diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx index c4e529f9f..6402c2bcd 100644 --- a/packages/bank-ui/src/pages/admin/AccountList.tsx +++ b/packages/bank-ui/src/pages/admin/AccountList.tsx @@ -51,19 +51,19 @@ export function AccountList({ if (result instanceof TalerError) { return <ErrorLoadingWithDebug error={result} />; } - if (result.data.type === "fail") { - switch (result.data.case) { + if (result.type === "fail") { + switch (result.case) { case HttpStatusCode.Unauthorized: return <Fragment />; default: - assertUnreachable(result.data.case); + assertUnreachable(result.case); } } const onGoStart = result.isFirstPage ? undefined : result.loadFirst; const onGoNext = result.isLastPage ? undefined : result.loadNext; - const accounts = result.result; + const accounts = result.body; return ( <Fragment> <div class="px-4 sm:px-6 lg:px-8 mt-8"> diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx index 94b88dc89..4784fc73a 100644 --- a/packages/bank-ui/src/pages/admin/AdminHome.tsx +++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx @@ -26,6 +26,7 @@ import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, getDate, + getDaysInMonth, getHours, getMonth, getYear, @@ -127,8 +128,8 @@ export function getTimeframesForDate( }; case TalerCorebankApi.MonitorTimeframeParam.day: return { - current: getDate(sub(time, { days: 1 })), - previous: getDate(sub(time, { days: 2 })), + current: getDaysInMonth(sub(time, { days: 1 })), + previous: getDaysInMonth(sub(time, { days: 2 })), }; case TalerCorebankApi.MonitorTimeframeParam.month: return { diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx index 1fccc2637..9e801a233 100644 --- a/packages/merchant-backoffice-ui/src/Routing.tsx +++ b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -158,7 +158,7 @@ export function Routing(_p: Props): VNode { const now = AbsoluteTime.now(); const instance = useInstanceBankAccounts(); - const accounts = !instance.ok ? undefined : instance.data.accounts; + const accounts = !instance || instance instanceof TalerError || instance.data.type === "fail" ? undefined : instance.result; const shouldWarnAboutMissingBankAccounts = !state.isAdmin && accounts !== undefined && accounts.length < 1 && (AbsoluteTime.isNever(preference.hideMissingAccountUntil) || diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts deleted file mode 100644 index 8c54f70db..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ /dev/null @@ -1,373 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - TalerErrorDetail, - TalerMerchantApi -} from "@gnu-taler/taler-util"; -import { - HttpResponse, - HttpResponseOk, - RequestError, - RequestOptions, - useApiContext -} from "@gnu-taler/web-util/browser"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useSWRConfig } from "swr"; -import { useSessionContext } from "../context/session.js"; - -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) { - return mutate( - (key) => { - // evict if no key or regex === all - if (!key || !re) return true; - // match string - if (typeof key === "string" && re.test(key)) return true; - // record or object have the path at [0] - if (typeof key === "object" && re.test(key[0])) return true; - //key didn't match regex - return false; - }, - undefined, - { - revalidate: true, - }, - ); - }; -} - -export function useBackendInstancesTestForAdmin(): HttpResponse< - TalerMerchantApi.InstancesResponse, - TalerErrorDetail -> { - const { request } = useBackendBaseRequest(); - - type Type = TalerMerchantApi.InstancesResponse; - - const [result, setResult] = useState< - HttpResponse<Type, TalerErrorDetail> - >({ loading: true }); - - useEffect(() => { - request<Type>(`/management/instances`) - .then((data) => setResult(data)) - .catch((error: RequestError<TalerErrorDetail>) => - setResult(error.cause), - ); - }, [request]); - - return result; -} - -const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; -const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; - -export function useBackendConfig(): HttpResponse< - TalerMerchantApi.VersionResponse | undefined, - RequestError<TalerErrorDetail> -> { - const { request } = useBackendBaseRequest(); - - type Type = TalerMerchantApi.VersionResponse; - type State = { - data: HttpResponse<Type, RequestError<TalerErrorDetail>>; - timer: number; - }; - const [result, setResult] = useState<State>({ - data: { loading: true }, - timer: 0, - }); - - useEffect(() => { - if (result.timer) { - clearTimeout(result.timer); - } - function tryConfig(): void { - request<Type>(`/config`) - .then((data) => { - const timer: any = setTimeout(() => { - tryConfig(); - }, CHECK_CONFIG_INTERVAL_OK); - setResult({ data, timer }); - }) - .catch((error) => { - const timer: any = setTimeout(() => { - tryConfig(); - }, CHECK_CONFIG_INTERVAL_FAIL); - const data = error.cause; - setResult({ data, timer }); - }); - } - tryConfig(); - }, [request]); - - return result.data; -} - -interface useBackendInstanceRequestType { - request: <T>( - endpoint: string, - options?: RequestOptions, - ) => Promise<HttpResponseOk<T>>; - fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; - orderFetcher: <T>( - params: [ - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, - ], - ) => Promise<HttpResponseOk<T>>; - transferFetcher: <T>( - params: [ - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, - ], - ) => Promise<HttpResponseOk<T>>; - templateFetcher: <T>( - params: [endpoint: string, position?: string, delta?: number], - ) => Promise<HttpResponseOk<T>>; - webhookFetcher: <T>( - params: [endpoint: string, position?: string, delta?: number], - ) => Promise<HttpResponseOk<T>>; -} -interface useBackendBaseRequestType { - request: <T>( - endpoint: string, - options?: RequestOptions, - ) => Promise<HttpResponseOk<T>>; -} - -type YesOrNo = "yes" | "no"; - -/** - * - * @param root the request is intended to the base URL and no the instance URL - * @returns request handler to - */ -export function useBackendBaseRequest(): useBackendBaseRequestType { - const { request: requestHandler } = useApiContext(); - const { state } = useSessionContext(); - const token = state.token; - const baseUrl = state.backendUrl; - - const request = useCallback( - function requestImpl<T>( - endpoint: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { ...options, token }) - .then((res) => { - return res; - }) - .catch((err) => { - throw err; - }); - }, - [baseUrl, token], - ); - - return { request }; -} - -export function useBackendInstanceRequest(): useBackendInstanceRequestType { - const { request: requestHandler } = useApiContext(); - - const { state } = useSessionContext(); - const token = state.token; - const baseUrl = state.backendUrl; - - const request = useCallback( - function requestImpl<T>( - endpoint: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token, ...options }); - }, - [baseUrl, token], - ); - - const multiFetcher = useCallback( - function multiFetcherImpl<T>( - args: [endpoints: string[]], - ): Promise<HttpResponseOk<T>[]> { - const [endpoints] = args; - return Promise.all( - endpoints.map((endpoint) => - requestHandler<T>(baseUrl, endpoint, { token }), - ), - ); - }, - [baseUrl, token], - ); - - const fetcher = useCallback( - function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token }); - }, - [baseUrl, token], - ); - - const orderFetcher = useCallback( - function orderFetcherImpl<T>( - args: [ - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, - ], - ): Promise<HttpResponseOk<T>> { - const [endpoint, paid, refunded, wired, searchDate, delta] = args; - const date_s = - delta && delta < 0 && searchDate - ? Math.floor(searchDate.getTime() / 1000) + 1 - : searchDate !== undefined - ? Math.floor(searchDate.getTime() / 1000) - : undefined; - const params: any = {}; - if (paid !== undefined) params.paid = paid; - if (delta !== undefined) params.delta = delta; - if (refunded !== undefined) params.refunded = refunded; - if (wired !== undefined) params.wired = wired; - if (date_s !== undefined) params.date_s = date_s; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { orders: [] } as T, - }); - } - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const transferFetcher = useCallback( - function transferFetcherImpl<T>( - args: [ - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, - ], - ): Promise<HttpResponseOk<T>> { - const [endpoint, payto_uri, verified, position, delta] = args; - const params: any = {}; - if (payto_uri !== undefined) params.payto_uri = payto_uri; - if (verified !== undefined) params.verified = verified; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { transfers: [] } as T, - }); - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const templateFetcher = useCallback( - function templateFetcherImpl<T>( - args: [endpoint: string, position?: string, delta?: number], - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args; - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { templates: [] } as T, - }); - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const webhookFetcher = useCallback( - function webhookFetcherImpl<T>( - args: [endpoint: string, position?: string, delta?: number], - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args; - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { webhooks: [] } as T, - }); - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - return { - request, - fetcher, - multiFetcher, - orderFetcher, - transferFetcher, - templateFetcher, - webhookFetcher, - }; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts index 9ad4c3069..e1f2638ed 100644 --- a/packages/merchant-backoffice-ui/src/hooks/bank.ts +++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -14,198 +14,97 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -// const MOCKED_ACCOUNTS: Record<string, TalerMerchantApi.AccountAddDetails> = { -// "hwire1": { -// h_wire: "hwire1", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe", -// }, -// "hwire2": { -// h_wire: "hwire2", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe2", -// }, -// } - -// export function useBankAccountAPI(): BankAccountAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const createBankAccount = async ( -// data: TalerMerchantApi.AccountAddDetails, -// ): Promise<HttpResponseOk<void>> => { -// // MOCKED_ACCOUNTS[data.h_wire] = data -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/accounts`, { -// method: "POST", -// data, -// }); -// await mutateAll(/.*private\/accounts.*/); -// return res; -// }; - -// const updateBankAccount = async ( -// h_wire: string, -// data: TalerMerchantApi.AccountPatchDetails, -// ): Promise<HttpResponseOk<void>> => { -// // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials -// // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/accounts/${h_wire}`, { -// method: "PATCH", -// data, -// }); -// await mutateAll(/.*private\/accounts.*/); -// return res; -// }; - -// const deleteBankAccount = async ( -// h_wire: string, -// ): Promise<HttpResponseOk<void>> => { -// // delete MOCKED_ACCOUNTS[h_wire] -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/accounts/${h_wire}`, { -// method: "DELETE", -// }); -// await mutateAll(/.*private\/accounts.*/); -// return res; -// }; - -// return { -// createBankAccount, -// updateBankAccount, -// deleteBankAccount, -// }; -// } - -// export interface BankAccountAPI { -// createBankAccount: ( -// data: TalerMerchantApi.AccountAddDetails, -// ) => Promise<HttpResponseOk<void>>; -// updateBankAccount: ( -// id: string, -// data: TalerMerchantApi.AccountPatchDetails, -// ) => Promise<HttpResponseOk<void>>; -// deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; -// } - export interface InstanceBankAccountFilter { } export function revalidateInstanceBankAccounts() { - // mutate(key => key instanceof) - return mutate((key) => Array.isArray(key) && key[key.length - 1] === "/private/accounts", undefined, { revalidate: true }); + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listBankAccounts", + undefined, + { revalidate: true }, + ); } -export function useInstanceBankAccounts( - args?: InstanceBankAccountFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - TalerMerchantApi.AccountsSummaryResponse, - TalerErrorDetail -> { - - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.AccountsSummaryResponse>, - RequestError<TalerErrorDetail> - >([`/private/accounts`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.AccountsSummaryResponse, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - 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.accounts.length < totalAfter; - const isReachingStart = false; +export function useInstanceBankAccounts() { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, bid]: [AccessToken, string]) { + return await management.listBankAccounts(token, { + limit: 5, + offset: bid, + order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listBankAccounts">, + TalerHttpError + >([session.token, offset, "listBankAccounts"], fetcher); + 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 = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.accounts.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] - .h_wire - }`; - if (from && updatePosition) updatePosition(from); - } + result, + isLastPage, + isFirstPage, + loadNext: () => { + if (!result.length) return; + setOffset(result[result.length - 1].h_wire); }, - loadMorePrev: () => { + loadFirst: () => { + setOffset(undefined); }, }; - const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { accounts } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { accounts }, ...pagination }; - } - return { loading: true }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire) +} + +export function revalidateBankAccountDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getBankAccountDetails", + undefined, + { revalidate: true }, + ); } +export function useBankAccountDetails(h_wire: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); -export function useBankAccountDetails( - h_wire: string, -): HttpResponse< - TalerMerchantApi.BankAccountEntry, - TalerErrorDetail -> { - // return { - // ok: true, - // data: { - // ...MOCKED_ACCOUNTS[h_wire], - // active: true, - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.BankAccountEntry>, - RequestError<TalerErrorDetail> - >([`/private/accounts/${h_wire}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; + async function fetcher([token, wireId]: [AccessToken, string]) { + return await management.getBankAccountDetails(token, wireId); } - if (error) return error.cause; - return { loading: true }; + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getBankAccountDetails">, + TalerHttpError + >([session.token, h_wire, "getBankAccountDetails"], fetcher); + + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index e8a431ae5..cc907bd8f 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -14,18 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - RequestError, useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { - useBackendBaseRequest, - useBackendInstanceRequest -} from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { AccessToken, TalerErrorDetail, TalerHttpError, TalerMerchantApi, TalerMerchantInstanceResultByMethod, TalerMerchantManagementResultByMethod, TalerMerchantResultByMethod } from "@gnu-taler/taler-util"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -39,28 +32,6 @@ export function revalidateInstanceDetails() { ); } export function useInstanceDetails() { - // const { fetcher } = useBackendInstanceRequest(); - - // const { data, error, isValidating } = useSWR< - // HttpResponseOk<TalerMerchantApi.QueryInstancesResponse>, - // RequestError<TalerErrorDetail> - // >([`/private/`], fetcher, { - // refreshInterval: 0, - // refreshWhenHidden: false, - // revalidateOnFocus: false, - // revalidateOnReconnect: false, - // refreshWhenOffline: false, - // revalidateIfStale: false, - // errorRetryCount: 0, - // errorRetryInterval: 1, - // shouldRetryOnError: false, - // }); - - // if (isValidating) return { loading: true, data: data?.data }; - // if (data) return data; - // if (error) return error.cause; - // return { loading: true }; - const { state: session } = useSessionContext(); const { lib: { management } } = useMerchantApiContext(); @@ -78,9 +49,6 @@ export function useInstanceDetails() { return undefined; } -// type KYCStatus = -// | { type: "ok" } -// | { type: "redirect"; status: TalerMerchantApi.AccountKycRedirects }; export function revalidateInstanceKYCDetails() { return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus", @@ -89,32 +57,6 @@ export function revalidateInstanceKYCDetails() { ); } export function useInstanceKYCDetails() { - // const { fetcher } = useBackendInstanceRequest(); - - // const { data, error } = useSWR< - // HttpResponseOk<TalerMerchantApi.AccountKycRedirects>, - // RequestError<TalerErrorDetail> - // >([`/private/kyc`], fetcher, { - // refreshInterval: 60 * 1000, - // refreshWhenHidden: false, - // revalidateOnFocus: false, - // revalidateIfStale: false, - // revalidateOnMount: false, - // revalidateOnReconnect: false, - // refreshWhenOffline: false, - // errorRetryCount: 0, - // errorRetryInterval: 1, - // shouldRetryOnError: false, - // }); - - // if (data) { - // if (data.info?.status === 202) - // return { ok: true, data: { type: "redirect", status: data.data } }; - // return { ok: true, data: { type: "ok" } }; - // } - // if (error) return error.cause; - // return { loading: true }; - const { state: session } = useSessionContext(); const { lib: { management } } = useMerchantApiContext(); diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts index 40932ac62..47ddf1c38 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -14,276 +14,81 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest } from "./backend.js"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import { AbsoluteTime, AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -// export interface OrderAPI { -// //FIXME: add OutOfStockResponse on 410 -// createOrder: ( -// data: TalerMerchantApi.PostOrderRequest, -// ) => Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>>; -// forgetOrder: ( -// id: string, -// data: TalerMerchantApi.ForgetRequest, -// ) => Promise<HttpResponseOk<void>>; -// refundOrder: ( -// id: string, -// data: TalerMerchantApi.RefundRequest, -// ) => Promise<HttpResponseOk<TalerMerchantApi.MerchantRefundResponse>>; -// deleteOrder: (id: string) => Promise<HttpResponseOk<void>>; -// getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>; -// } -type YesOrNo = "yes" | "no"; -// export function useOrderAPI(): OrderAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const createOrder = async ( -// data: TalerMerchantApi.PostOrderRequest, -// ): Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>> => { -// const res = await request<TalerMerchantApi.PostOrderResponse>( -// `/private/orders`, -// { -// method: "POST", -// data, -// }, -// ); -// await mutateAll(/.*private\/orders.*/); -// // mutate('') -// return res; -// }; -// const refundOrder = async ( -// orderId: string, -// data: TalerMerchantApi.RefundRequest, -// ): Promise<HttpResponseOk<TalerMerchantApi.MerchantRefundResponse>> => { -// mutateAll(/@"\/private\/orders"@/); -// const res = request<TalerMerchantApi.MerchantRefundResponse>( -// `/private/orders/${orderId}/refund`, -// { -// method: "POST", -// data, -// }, -// ); - -// // order list returns refundable information, so we must evict everything -// await mutateAll(/.*private\/orders.*/); -// return res; -// }; - -// const forgetOrder = async ( -// orderId: string, -// data: TalerMerchantApi.ForgetRequest, -// ): Promise<HttpResponseOk<void>> => { -// mutateAll(/@"\/private\/orders"@/); -// const res = request<void>(`/private/orders/${orderId}/forget`, { -// method: "PATCH", -// data, -// }); -// // we may be forgetting some fields that are pare of the listing, so we must evict everything -// await mutateAll(/.*private\/orders.*/); -// return res; -// }; -// const deleteOrder = async ( -// orderId: string, -// ): Promise<HttpResponseOk<void>> => { -// mutateAll(/@"\/private\/orders"@/); -// const res = request<void>(`/private/orders/${orderId}`, { -// method: "DELETE", -// }); -// await mutateAll(/.*private\/orders.*/); -// return res; -// }; - -// const getPaymentURL = async ( -// orderId: string, -// ): Promise<HttpResponseOk<string>> => { -// return request<TalerMerchantApi.MerchantOrderStatusResponse>( -// `/private/orders/${orderId}`, -// { -// method: "GET", -// }, -// ).then((res) => { -// const url = -// res.data.order_status === "unpaid" -// ? res.data.taler_pay_uri -// : res.data.contract_terms.fulfillment_url; -// const response: HttpResponseOk<string> = res as any; -// response.data = url || ""; -// return response; -// }); -// }; - -// return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL }; -// } +export function revalidateOrderDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getOrderDetails", + undefined, + { revalidate: true }, + ); +} +export function useOrderDetails(oderId: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); -export function useOrderDetails( - oderId: string, -): HttpResponse< - TalerMerchantApi.MerchantOrderStatusResponse, - TalerErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([dId, token]: [string, AccessToken]) { + return await management.getOrderDetails(token, dId); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.MerchantOrderStatusResponse>, - RequestError<TalerErrorDetail> - >([`/private/orders/${oderId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getOrderDetails">, + TalerHttpError + >([oderId, session.token, "getOrderDetails"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } export interface InstanceOrderFilter { - paid?: YesOrNo; - refunded?: YesOrNo; - wired?: YesOrNo; - date?: Date; + paid?: boolean; + refunded?: boolean; + wired?: boolean; + date?: AbsoluteTime; + position?: string; } export function useInstanceOrders( args?: InstanceOrderFilter, - updateFilter?: (d: Date) => void, -): HttpResponsePaginated< - TalerMerchantApi.OrderHistory, - TalerErrorDetail -> { - const { orderFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<TalerMerchantApi.OrderHistory>, - RequestError<TalerErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - totalBefore, - ], - orderFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.OrderHistory>, - RequestError<TalerErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - -totalAfter, - ], - orderFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - TalerMerchantApi.OrderHistory, - TalerErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.OrderHistory, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); - - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + updatePosition: (d: string | undefined) => void = () => { }, +) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + // const [offset, setOffset] = useState<string | undefined>(args?.position); + + async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) { + return await management.listOrders(token, { + limit: PAGE_SIZE, + offset: o, + order: "dec", + paid: p, + refunded: r, + wired: w, + date: d, + }); + } - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; - const isReachingStart = - args?.date === undefined || - (beforeData && beforeData.data.orders.length < totalBefore); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listOrders">, + TalerHttpError + >([session.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher); - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.orders.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = - afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.orders.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = - beforeData.data.orders[beforeData.data.orders.length - 1].timestamp - .t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const orders = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.orders - .slice() - .reverse() - .concat((afterData || lastAfter).data.orders); - if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; - if (beforeData && afterData) { - return { ok: true, data: { orders }, ...pagination }; - } - return { loading: true }; + return buildPaginatedResult(data.body.orders, args?.position, updatePosition, (d) => String(d.row_id)) } diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts index 36db2ea90..69e4a0f4f 100644 --- a/packages/merchant-backoffice-ui/src/hooks/otp.ts +++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -14,195 +14,72 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -// export function useOtpDeviceAPI(): OtpDeviceAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const createOtpDevice = async ( -// data: TalerMerchantApi.OtpDeviceAddDetails, -// ): Promise<HttpResponseOk<void>> => { -// // MOCKED_DEVICES[data.otp_device_id] = data -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/otp-devices`, { -// method: "POST", -// data, -// }); -// await mutateAll(/.*private\/otp-devices.*/); -// return res; -// }; - -// const updateOtpDevice = async ( -// deviceId: string, -// data: TalerMerchantApi.OtpDevicePatchDetails, -// ): Promise<HttpResponseOk<void>> => { -// // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm -// // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr -// // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description -// // MOCKED_DEVICES[deviceId].otp_key = data.otp_key -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/otp-devices/${deviceId}`, { -// method: "PATCH", -// data, -// }); -// await mutateAll(/.*private\/otp-devices.*/); -// return res; -// }; - -// const deleteOtpDevice = async ( -// deviceId: string, -// ): Promise<HttpResponseOk<void>> => { -// // delete MOCKED_DEVICES[deviceId] -// // return Promise.resolve({ ok: true, data: undefined }); -// const res = await request<void>(`/private/otp-devices/${deviceId}`, { -// method: "DELETE", -// }); -// await mutateAll(/.*private\/otp-devices.*/); -// return res; -// }; - -// return { -// createOtpDevice, -// updateOtpDevice, -// deleteOtpDevice, -// }; -// } - -// export interface OtpDeviceAPI { -// createOtpDevice: ( -// data: TalerMerchantApi.OtpDeviceAddDetails, -// ) => Promise<HttpResponseOk<void>>; -// updateOtpDevice: ( -// id: string, -// data: TalerMerchantApi.OtpDevicePatchDetails, -// ) => Promise<HttpResponseOk<void>>; -// deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; -// } - -export interface InstanceOtpDeviceFilter { +export function revalidateInstanceOtpDevices() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listOtpDevices", + undefined, + { revalidate: true }, + ); } +export function useInstanceOtpDevices() { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, bid]: [AccessToken, string]) { + return await management.listOtpDevices(token, { + limit: PAGE_SIZE, + offset: bid, + order: "dec", + }); + } -export function useInstanceOtpDevices( - args?: InstanceOtpDeviceFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - TalerMerchantApi.OtpDeviceSummaryResponse, - TalerErrorDetail -> { - // return { - // ok: true, - // loadMore: () => { }, - // loadMorePrev: () => { }, - // data: { - // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ - // device_description: d.otp_device_description, - // otp_device_id: d.otp_device_id - // })) - // } - // } - - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.OtpDeviceSummaryResponse>, - RequestError<TalerErrorDetail> - >([`/private/otp-devices`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.OtpDeviceSummaryResponse, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - if (afterError) return afterError.cause; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listOtpDevices">, + TalerHttpError + >([session.token, offset, "listOtpDevices"], fetcher); - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.otp_devices.length < totalAfter; - const isReachingStart = true; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] - .otp_device_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; + return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id) +} - const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { otp_devices } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { otp_devices }, ...pagination }; - } - return { loading: true }; +export function revalidateOtpDeviceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails", + undefined, + { revalidate: true }, + ); } +export function useOtpDeviceDetails(deviceId: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); -export function useOtpDeviceDetails( - deviceId: string, -): HttpResponse< - TalerMerchantApi.OtpDeviceDetails, - TalerErrorDetail -> { - // return { - // ok: true, - // data: { - // device_description: MOCKED_DEVICES[deviceId].otp_device_description, - // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, - // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr - // } - // } - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([dId, token]: [string, AccessToken]) { + return await management.getOtpDeviceDetails(token, dId); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.OtpDeviceDetails>, - RequestError<TalerErrorDetail> - >([`/private/otp-devices/${deviceId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">, + TalerHttpError + >([deviceId, session.token, "getOtpDeviceDetails"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts index 0eb54f717..6721136a5 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -14,164 +14,93 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { AccessToken, OperationOk, TalerHttpError, TalerMerchantApi, TalerMerchantManagementErrorsByMethod, TalerMerchantManagementResultByMethod, opFixedSuccess } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { PAGE_SIZE } from "../utils/constants.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -// export interface ProductAPI { -// getProduct: ( -// id: string, -// ) => Promise<void>; -// createProduct: ( -// data: TalerMerchantApi.ProductAddDetail, -// ) => Promise<void>; -// updateProduct: ( -// id: string, -// data: TalerMerchantApi.ProductPatchDetail, -// ) => Promise<void>; -// deleteProduct: (id: string) => Promise<void>; -// lockProduct: ( -// id: string, -// data: TalerMerchantApi.LockRequest, -// ) => Promise<void>; -// } - -// export function useProductAPI(): ProductAPI { -// const mutateAll = useMatchMutate(); -// const { mutate } = useSWRConfig(); - -// const { request } = useBackendInstanceRequest(); - -// const createProduct = async ( -// data: TalerMerchantApi.ProductAddDetail, -// ): Promise<void> => { -// const res = await request(`/private/products`, { -// method: "POST", -// data, -// }); - -// return await mutateAll(/.*\/private\/products.*/); -// }; - -// const updateProduct = async ( -// productId: string, -// data: TalerMerchantApi.ProductPatchDetail, -// ): Promise<void> => { -// const r = await request(`/private/products/${productId}`, { -// method: "PATCH", -// data, -// }); - -// return await mutateAll(/.*\/private\/products.*/); -// }; - -// const deleteProduct = async (productId: string): Promise<void> => { -// await request(`/private/products/${productId}`, { -// method: "DELETE", -// }); -// await mutate([`/private/products`]); -// }; - -// const lockProduct = async ( -// productId: string, -// data: TalerMerchantApi.LockRequest, -// ): Promise<void> => { -// await request(`/private/products/${productId}/lock`, { -// method: "POST", -// data, -// }); - -// return await mutateAll(/.*"\/private\/products.*/); -// }; - -// const getProduct = async ( -// productId: string, -// ): Promise<void> => { -// await request(`/private/products/${productId}`, { -// method: "GET", -// }); - -// return -// }; - -// return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; -// } - -export function useInstanceProducts(): HttpResponse< - (TalerMerchantApi.ProductDetail & WithId)[], - TalerErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<TalerMerchantApi.InventorySummaryResponse>, - RequestError<TalerErrorDetail> - >([`/private/products`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - const paths = (list?.data.products || []).map( - (p) => `/private/products/${p.product_id}`, +type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number }; +function notUndefined(c: ProductWithId | undefined): c is ProductWithId { + return c !== undefined; +} + +export function revalidateInstanceProducts() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listProductsWithId", + undefined, + { revalidate: true }, ); - const { data: products, error: productError } = useSWR< - HttpResponseOk<TalerMerchantApi.ProductDetail>[], - RequestError<TalerErrorDetail> - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - if (productError) return productError.cause; - - if (products) { - const dataWithId = products.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/private\/products\//, "") || "", - }; +} +export function useInstanceProducts() { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + const [offset, setOffset] = useState<number | undefined>(); + + async function fetcher([token, bid]: [AccessToken, number]) { + const list = await management.listProducts(token, { + limit: PAGE_SIZE, + offset: String(bid), + order: "dec", }); - return { ok: true, data: dataWithId }; + if (list.type !== "ok") { + return list; + } + const all: Array<ProductWithId | undefined> = await Promise.all( + list.body.products.map(async (c) => { + const r = await management.getProductDetails(token, c.product_id); + if (r.type === "fail") { + return undefined; + } + return { ...r.body, id: c.product_id, serial: c.product_serial }; + }), + ); + const products = all.filter(notUndefined); + + return opFixedSuccess({ products }); } - return { loading: true }; + + const { data, error } = useSWR< + OperationOk<{ products: ProductWithId[] }> | + TalerMerchantManagementErrorsByMethod<"listProducts">, + TalerHttpError + >([session.token, offset, "listProductsWithId"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial) +} + +export function revalidateProductDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getProductDetails", + undefined, + { revalidate: true }, + ); } +export function useProductDetails(productId: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + async function fetcher([pid, token]: [string, AccessToken]) { + return await management.getProductDetails(token, pid); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getProductDetails">, + TalerHttpError + >([productId, session.token, "getProductDetails"], fetcher); -export function useProductDetails( - productId: string, -): HttpResponse< - TalerMerchantApi.ProductDetail, - TalerErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.ProductDetail>, - RequestError<TalerErrorDetail> - >([`/private/products/${productId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index ff0461a67..10e480b01 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -14,253 +14,77 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -// export function useTemplateAPI(): TemplateAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const createTemplate = async ( -// data: TalerMerchantApi.TemplateAddDetails, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/templates`, { -// method: "POST", -// data, -// }); -// await mutateAll(/.*private\/templates.*/); -// return res; -// }; - -// const updateTemplate = async ( -// templateId: string, -// data: TalerMerchantApi.TemplatePatchDetails, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/templates/${templateId}`, { -// method: "PATCH", -// data, -// }); -// await mutateAll(/.*private\/templates.*/); -// return res; -// }; - -// const deleteTemplate = async ( -// templateId: string, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/templates/${templateId}`, { -// method: "DELETE", -// }); -// await mutateAll(/.*private\/templates.*/); -// return res; -// }; - -// const createOrderFromTemplate = async ( -// templateId: string, -// data: TalerMerchantApi.UsingTemplateDetails, -// ): Promise< -// HttpResponseOk<TalerMerchantApi.PostOrderResponse> -// > => { -// const res = await request<TalerMerchantApi.PostOrderResponse>( -// `/templates/${templateId}`, -// { -// method: "POST", -// data, -// }, -// ); -// await mutateAll(/.*private\/templates.*/); -// return res; -// }; - -// const testTemplateExist = async ( -// templateId: string, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); -// return res; -// }; - - -// return { -// createTemplate, -// updateTemplate, -// deleteTemplate, -// testTemplateExist, -// createOrderFromTemplate, -// }; -// } - -// export interface TemplateAPI { -// createTemplate: ( -// data: TalerMerchantApi.TemplateAddDetails, -// ) => Promise<HttpResponseOk<void>>; -// updateTemplate: ( -// id: string, -// data: TalerMerchantApi.TemplatePatchDetails, -// ) => Promise<HttpResponseOk<void>>; -// testTemplateExist: ( -// id: string -// ) => Promise<HttpResponseOk<void>>; -// deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; -// createOrderFromTemplate: ( -// id: string, -// data: TalerMerchantApi.UsingTemplateDetails, -// ) => Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>>; -// } export interface InstanceTemplateFilter { - //FIXME: add filter to the template list - position?: string; } -export function useInstanceTemplates( - args?: InstanceTemplateFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - TalerMerchantApi.TemplateSummaryResponse, - TalerErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<TalerMerchantApi.TemplateSummaryResponse>, - RequestError<TalerErrorDetail>>( - [ - `/private/templates`, - args?.position, - totalBefore, - ], - templateFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.TemplateSummaryResponse>, - RequestError<TalerErrorDetail> - >([`/private/templates`, args?.position, -totalAfter], templateFetcher); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - TalerMerchantApi.TemplateSummaryResponse, - TalerErrorDetail - > - >({ loading: true }); +export function revalidateInstanceTemplates() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listTemplates", + undefined, + { revalidate: true }, + ); +} +export function useInstanceTemplates() { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, bid]: [AccessToken, string]) { + return await management.listTemplates(token, { + limit: PAGE_SIZE, + offset: bid, + order: "dec", + }); + } - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.TemplateSummaryResponse, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listTemplates">, + TalerHttpError + >([session.token, offset, "listTemplates"], fetcher); - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.templates.length < totalAfter; - const isReachingStart = args?.position === undefined - || - (beforeData && beforeData.data.templates.length < totalBefore); + return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id) - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.templates.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.templates[afterData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.templates.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; +} - // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; - const templates = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.templates - .slice() - .reverse() - .concat((afterData || lastAfter).data.templates); - if (loadingAfter || loadingBefore) - return { loading: true, data: { templates } }; - if (beforeData && afterData) { - return { ok: true, data: { templates }, ...pagination }; - } - return { loading: true }; +export function revalidateProductDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getTemplateDetails", + undefined, + { revalidate: true }, + ); } +export function useTemplateDetails(templateId: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); -export function useTemplateDetails( - templateId: string, -): HttpResponse< - TalerMerchantApi.TemplateDetails, - TalerErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); + async function fetcher([tid, token]: [string, AccessToken]) { + return await management.getTemplateDetails(token, tid); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.TemplateDetails>, - RequestError<TalerErrorDetail> - >([`/private/templates/${templateId}`], templateFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getTemplateDetails">, + TalerHttpError + >([templateId, session.token, "getTemplateDetails"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts index af62af0fa..afabf775c 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -14,173 +14,59 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; +import { useState } from "preact/hooks"; const useSWR = _useSWR as unknown as SWRHook; -// export function useTransferAPI(): TransferAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const informTransfer = async ( -// data: TalerMerchantApi.TransferInformation, -// ): Promise<HttpResponseOk<{}>> => { -// const res = await request<{}>(`/private/transfers`, { -// method: "POST", -// data, -// }); - -// await mutateAll(/.*private\/transfers.*/); -// return res; -// }; - -// return { informTransfer }; -// } - -// export interface TransferAPI { -// informTransfer: ( -// data: TalerMerchantApi.TransferInformation, -// ) => Promise<HttpResponseOk<{}>>; -// } - export interface InstanceTransferFilter { payto_uri?: string; - verified?: "yes" | "no"; + verified?: boolean; position?: string; } +export function revalidateInstanceTransfers() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listWireTransfers", + undefined, + { revalidate: true }, + ); +} export function useInstanceTransfers( args?: InstanceTransferFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - TalerMerchantApi.TransferList, - TalerErrorDetail -> { - const { transferFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<TalerMerchantApi.TransferList>, - RequestError<TalerErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - totalBefore, - ], - transferFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.TransferList>, - RequestError<TalerErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - -totalAfter, - ], - transferFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - TalerMerchantApi.TransferList, - TalerErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.TransferList, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); + updatePosition: (id: string | undefined) => void = (() => { }), +) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + // const [offset, setOffset] = useState<string | undefined>(args?.position); + + async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) { + return await management.listWireTransfers(token, { + paytoURI: p, + verified: v, + limit: PAGE_SIZE, + offset: o, + order: "dec", + }); + } - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listWireTransfers">, + TalerHttpError + >([session.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher); - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.transfers.length < totalAfter; - const isReachingStart = - args?.position === undefined || - (beforeData && beforeData.data.transfers.length < totalBefore); + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.transfers.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.transfers[afterData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${beforeData.data.transfers[beforeData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; + return buildPaginatedResult(data.body.transfers, args?.position, updatePosition, (d) => String(d.transfer_serial_id)) - const transfers = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.transfers - .slice() - .reverse() - .concat((afterData || lastAfter).data.transfers); - if (loadingAfter || loadingBefore) - return { loading: true, data: { transfers } }; - if (beforeData && afterData) { - return { ok: true, data: { transfers }, ...pagination }; - } - return { loading: true }; } diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts index cc817b84e..5e2e08bcc 100644 --- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts +++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -14,165 +14,108 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGE_SIZE } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, OperationOk, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -// export function useWebhookAPI(): WebhookAPI { -// const mutateAll = useMatchMutate(); -// const { request } = useBackendInstanceRequest(); - -// const createWebhook = async ( -// data: TalerMerchantApi.WebhookAddDetails, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/webhooks`, { -// method: "POST", -// data, -// }); -// await mutateAll(/.*private\/webhooks.*/); -// return res; -// }; - -// const updateWebhook = async ( -// webhookId: string, -// data: TalerMerchantApi.WebhookPatchDetails, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/webhooks/${webhookId}`, { -// method: "PATCH", -// data, -// }); -// await mutateAll(/.*private\/webhooks.*/); -// return res; -// }; - -// const deleteWebhook = async ( -// webhookId: string, -// ): Promise<HttpResponseOk<void>> => { -// const res = await request<void>(`/private/webhooks/${webhookId}`, { -// method: "DELETE", -// }); -// await mutateAll(/.*private\/webhooks.*/); -// return res; -// }; - -// return { createWebhook, updateWebhook, deleteWebhook }; -// } - -// export interface WebhookAPI { -// createWebhook: ( -// data: TalerMerchantApi.WebhookAddDetails, -// ) => Promise<HttpResponseOk<void>>; -// updateWebhook: ( -// id: string, -// data: TalerMerchantApi.WebhookPatchDetails, -// ) => Promise<HttpResponseOk<void>>; -// deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; -// } - export interface InstanceWebhookFilter { - //FIXME: add filter to the webhook list - position?: string; } -export function useInstanceWebhooks( - args?: InstanceWebhookFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - TalerMerchantApi.WebhookSummaryResponse, - TalerErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<TalerMerchantApi.WebhookSummaryResponse>, - RequestError<TalerErrorDetail> - - >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - TalerMerchantApi.WebhookSummaryResponse, - TalerErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) return afterError.cause; - - const isReachingEnd = - afterData && afterData.data.webhooks.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.webhooks.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id - }`; - if (from && updatePosition) updatePosition(from); - } +export function revalidateInstanceWebhooks() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listWebhooks", + undefined, + { revalidate: true }, + ); +} +export function useInstanceWebhooks() { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, bid]: [AccessToken, string]) { + return await management.listWebhooks(token, { + limit: 5, + offset: bid, + order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listWebhooks">, + TalerHttpError + >([session.token, offset, "listWebhooks"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return buildPaginatedResult(data.body.webhooks, offset, setOffset, (d) => d.webhook_id) +} + +type PaginatedResult<T> = OperationOk<T> & { + isLastPage: boolean; + isFirstPage: boolean; + loadNext(): void; + loadFirst(): void; +} + +//TODO: consider sending this to web-util +export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> { + + const isLastPage = data.length <= PAGE_SIZE; + const isFirstPage = offset === undefined; + + const result = structuredClone(data); + if (result.length == PAGE_SIZE + 1) { + result.pop(); + } + return { + type: "ok", + body: result, + isLastPage, + isFirstPage, + loadNext: () => { + if (!result.length) return; + const id = getId(result[result.length - 1]) + setOffset(id); }, - loadMorePrev: () => { - return; + loadFirst: () => { + setOffset(undefined); }, }; +} - const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks; - if (loadingAfter) return { loading: true, data: { webhooks } }; - if (afterData) { - return { ok: true, data: { webhooks }, ...pagination }; - } - return { loading: true }; +export function revalidateWebhookDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getWebhookDetails", + undefined, + { revalidate: true }, + ); } +export function useWebhookDetails(webhookId: string) { + const { state: session } = useSessionContext(); + const { lib: { management } } = useMerchantApiContext(); + + async function fetcher([hookId, token]: [string, AccessToken]) { + return await management.getWebhookDetails(token, hookId); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getWebhookDetails">, + TalerHttpError + >([webhookId, session.token, "getWebhookDetails"], fetcher); -export function useWebhookDetails( - webhookId: string, -): HttpResponse< - TalerMerchantApi.WebhookDetails, - TalerErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<TalerMerchantApi.WebhookDetails>, - RequestError<TalerErrorDetail> - >([`/private/webhooks/${webhookId}`], webhookFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx index 35c9e6624..0ce126b76 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -46,7 +46,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} onCreate={(request: Entity) => { - return api.management.addAccount(state.token, request) + return api.management.addBankAccount(state.token, request) .then(() => { onConfirm() }) diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx index 8de6c763e..ab63d0d5f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util"; import { ErrorType, HttpError, @@ -34,6 +34,7 @@ import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { Notification } from "../../../../utils/types.js"; import { ListPage } from "./ListPage.js"; import { useSessionContext } from "../../../../context/session.js"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; interface Props { onUnauthorized: () => VNode; @@ -50,32 +51,21 @@ export default function ListOtpDevices({ onSelect, onNotFound, }: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { lib: api } = useMerchantApiContext(); const { state } = useSessionContext(); - const result = useInstanceBankAccounts({ position }, (id) => setPosition(id)); + const result = useInstanceBankAccounts(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> } return ( <Fragment> <NotificationCard notification={notif} /> - {result.data.accounts.length < 1 && + {result.result.length < 1 && <NotificationCard notification={{ type: "WARN", message: i18n.str`You need to associate a bank account to receive revenue.`, @@ -83,7 +73,7 @@ export default function ListOtpDevices({ }} /> } <ListPage - devices={result.data.accounts} + devices={result.result} onLoadMoreBefore={ result.isReachingStart ? result.loadMorePrev : undefined } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index dc2e08d91..ff54c487a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -83,7 +83,7 @@ export default function ProductList({ <JumpToElementById testIfExist={async (id) => { - const resp = await lib.management.getProduct(state.token, id); + const resp = await lib.management.getProductDetails(state.token, id); return resp.type === "ok"; }} onSelect={onSelect} diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts index 5fd001555..c223b0a16 100644 --- a/packages/taler-util/src/http-client/merchant.ts +++ b/packages/taler-util/src/http-client/merchant.ts @@ -512,7 +512,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts */ - async addAccount(token: AccessToken | undefined, body: TalerMerchantApi.AccountAddDetails) { + async addBankAccount(token: AccessToken | undefined, body: TalerMerchantApi.AccountAddDetails) { const url = new URL(`private/accounts`, this.baseUrl); const headers: Record<string, string> = {} @@ -540,7 +540,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async updateAccount( + async updateBankAccount( token: AccessToken | undefined, wireAccount: string, body: TalerMerchantApi.AccountPatchDetails, @@ -569,9 +569,11 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts */ - async listAccounts(token: AccessToken) { + async listBankAccounts(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/accounts`, this.baseUrl); + // addMerchantPaginationParams(url, params); + const headers: Record<string, string> = {} if (token) { headers.Authorization = makeBearerTokenAuthHeader(token) @@ -594,7 +596,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async getAccount(token: AccessToken | undefined, wireAccount: string) { + async getBankAccountDetails(token: AccessToken | undefined, wireAccount: string) { const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); const headers: Record<string, string> = {} @@ -619,7 +621,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async deleteAccount(token: AccessToken | undefined, wireAccount: string) { + async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) { const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); const headers: Record<string, string> = {} @@ -733,7 +735,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ - async getProduct(token: AccessToken | undefined, productId: string) { + async getProductDetails(token: AccessToken | undefined, productId: string) { const url = new URL(`private/products/${productId}`, this.baseUrl); const headers: Record<string, string> = {} @@ -902,7 +904,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID */ - async getOrder( + async getOrderDetails( token: AccessToken | undefined, orderId: string, params: TalerMerchantApi.GetOrderRequestParams = {}, @@ -1207,7 +1209,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices */ - async listOtpDevices(token: AccessToken | undefined,) { + async listOtpDevices(token: AccessToken | undefined, params?: PaginationParams) { const url = new URL(`private/otp-devices`, this.baseUrl); const headers: Record<string, string> = {} @@ -1231,7 +1233,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ - async getOtpDevice( + async getOtpDeviceDetails( token: AccessToken | undefined, deviceId: string, params: TalerMerchantApi.GetOtpDeviceRequestParams = {}, @@ -1350,7 +1352,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#inspecting-template */ - async listTemplates(token: AccessToken | undefined,) { + async listTemplates(token: AccessToken | undefined, params?: PaginationParams) { const url = new URL(`private/templates`, this.baseUrl); const headers: Record<string, string> = {} @@ -1374,7 +1376,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID */ - async getTemplate(token: AccessToken | undefined, templateId: string) { + async getTemplateDetails(token: AccessToken | undefined, templateId: string) { const url = new URL(`private/templates/${templateId}`, this.baseUrl); const headers: Record<string, string> = {} @@ -1520,7 +1522,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks */ - async listWebhooks(token: AccessToken | undefined,) { + async listWebhooks(token: AccessToken | undefined, params?: PaginationParams) { const url = new URL(`private/webhooks`, this.baseUrl); const headers: Record<string, string> = {} @@ -1545,7 +1547,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID */ - async getWebhook(token: AccessToken | undefined, webhookId: string) { + async getWebhookDetails(token: AccessToken | undefined, webhookId: string) { const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); const headers: Record<string, string> = {} @@ -1652,7 +1654,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies */ - async listTokenFamilies(token: AccessToken | undefined,) { + async listTokenFamilies(token: AccessToken | undefined, params?: PaginationParams) { const url = new URL(`private/tokenfamilies`, this.baseUrl); const headers: Record<string, string> = {} @@ -1677,7 +1679,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG */ - async getTokenFamily(token: AccessToken | undefined, tokenSlug: string) { + async getTokenFamilyDetails(token: AccessToken | undefined, tokenSlug: string) { const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); const headers: Record<string, string> = {} @@ -1853,7 +1855,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp /** * https://docs.taler.net/core/api-merchant.html#get--management-instances */ - async listInstances(token: AccessToken | undefined,) { + async listInstances(token: AccessToken | undefined, params?: PaginationParams) { const url = new URL(`management/instances`, this.baseUrl); const headers: Record<string, string> = {} |