diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/hooks')
24 files changed, 1240 insertions, 3671 deletions
diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts index f22badc88..212ef2211 100644 --- a/packages/merchant-backoffice-ui/src/hooks/async.ts +++ b/packages/merchant-backoffice-ui/src/hooks/async.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 8d99546a8..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - HttpResponse, - HttpResponseOk, - RequestError, - RequestOptions, - useApiContext, -} from "@gnu-taler/web-util/browser"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; -import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.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< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - type Type = MerchantBackend.Instances.InstancesResponse; - - const [result, setResult] = useState< - HttpResponse<Type, MerchantBackend.ErrorDetail> - >({ loading: true }); - - useEffect(() => { - request<Type>(`/management/instances`) - .then((data) => setResult(data)) - .catch((error: RequestError<MerchantBackend.ErrorDetail>) => - 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< - MerchantBackend.VersionResponse | undefined, - RequestError<MerchantBackend.ErrorDetail> -> { - const { request } = useBackendBaseRequest(); - - type Type = MerchantBackend.VersionResponse; - type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, 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>>; - reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - rewardsDetailFetcher: <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"; -type LoginResult = { - valid: true; - token: string; - expiration: Timestamp; -} | { - valid: false; - cause: HttpError<{}>; -} - -export function useCredentialsChecker() { - const { request } = useApiContext(); - //check against instance details endpoint - //while merchant backend doesn't have a login endpoint - async function requestNewLoginToken( - baseUrl: string, - token: AccessToken, - ): Promise<LoginResult> { - const data: MerchantBackend.Instances.LoginTokenRequest = { - scope: "write", - duration: { - d_us: "forever" - }, - refreshable: true, - } - try { - const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, { - method: "POST", - token, - data - }); - return { valid: true, token: response.data.token, expiration: response.data.expiration }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, cause: error.cause }; - } - - return { - valid: false, cause: { - type: ErrorType.UNEXPECTED, - loading: false, - info: { - hasToken: true, - status: 0, - options: {}, - url: `/private/token`, - payload: {} - }, - exception: error, - message: (error instanceof Error ? error.message : "unpexepected error") - } - }; - } - }; - - async function refreshLoginToken( - baseUrl: string, - token: LoginToken - ): Promise<LoginResult> { - - if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { - return { - valid: false, cause: { - type: ErrorType.CLIENT, - status: HttpStatusCode.Unauthorized, - message: "login token expired, login again.", - info: { - hasToken: true, - status: 401, - options: {}, - url: `/private/token`, - payload: {} - }, - payload: {} - }, - } - } - - return requestNewLoginToken(baseUrl, token.token as AccessToken) - } - return { requestNewLoginToken, refreshLoginToken } -} - -/** - * - * @param root the request is intended to the base URL and no the instance URL - * @returns request handler to - */ -export function useBackendBaseRequest(): useBackendBaseRequestType { - const { url: backend, token: loginToken } = useBackendContext(); - const { request: requestHandler } = useApiContext(); - const token = loginToken?.token; - - const request = useCallback( - function requestImpl<T>( - endpoint: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => { - return res - }).catch(err => { - throw err - }); - }, - [backend, token], - ); - - return { request }; -} - -export function useBackendInstanceRequest(): useBackendInstanceRequestType { - const { url: rootBackendUrl, token: rootToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); - const { request: requestHandler } = useApiContext(); - - const { baseUrl, token: loginToken } = !admin - ? { baseUrl: rootBackendUrl, token: rootToken } - : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; - - const token = loginToken?.token; - - 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 reserveDetailFetcher = useCallback( - function reserveDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - rewards: "yes", - }, - token, - }); - }, - [baseUrl, token], - ); - - const rewardsDetailFetcher = useCallback( - function rewardsDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - pickups: "yes", - }, - 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, - reserveDetailFetcher, - rewardsDetailFetcher, - transferFetcher, - templateFetcher, - webhookFetcher, - }; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts index 03b064646..8857ad839 100644 --- a/packages/merchant-backoffice-ui/src/hooks/bank.ts +++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,204 +14,73 @@ 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 { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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"; const useSWR = _useSWR as unknown as SWRHook; -// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.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 interface InstanceBankAccountFilter { +} -export function useBankAccountAPI(): BankAccountAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); +export function revalidateInstanceBankAccounts() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listBankAccounts", + undefined, + { revalidate: true }, + ); +} +export function useInstanceBankAccounts() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - const createBankAccount = async ( - data: MerchantBackend.BankAccounts.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 [offset, setOffset] = useState<string | undefined>(); - const updateBankAccount = async ( - h_wire: string, - data: MerchantBackend.BankAccounts.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, + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listBankAccounts(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - 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; - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listBankAccounts">, + TalerHttpError + >([session.token, "offset", "listBankAccounts"], fetcher); - return { - createBankAccount, - updateBankAccount, - deleteBankAccount, - }; -} + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; -export interface BankAccountAPI { - createBankAccount: ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateBankAccount: ( - id: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; + // return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire) + return data; } -export interface InstanceBankAccountFilter { +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: { instance } } = useSessionContext(); -export function useInstanceBankAccounts( - args?: InstanceBankAccountFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore() { }, - // loadMorePrev() { }, - // data: { - // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ - // ...e, - // active: true, - // })) - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ 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; - - 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); - } - }, - loadMorePrev: () => { - }, - }; - - const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { accounts } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { accounts }, ...pagination }; + async function fetcher([token, wireId]: [AccessToken, string]) { + return await instance.getBankAccountDetails(token, wireId); } - return { loading: true }; -} -export function useBankAccountDetails( - h_wire: string, -): HttpResponse< - MerchantBackend.BankAccounts.BankAccountEntry, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // ...MOCKED_ACCOUNTS[h_wire], - // active: true, - // } - // } - const { fetcher } = useBackendInstanceRequest(); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getBankAccountDetails">, + TalerHttpError + >([session.token, h_wire, "getBankAccountDetails"], fetcher); - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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; - } - 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/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts deleted file mode 100644 index f0cd1bfb9..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; -import { ValueOrFunction } from "../utils/types.js"; -import { useMatchMutate } from "./backend.js"; - -const calculateRootPath = () => { - const rootPath = - typeof window !== undefined - ? window.location.origin + window.location.pathname - : "/"; - - /** - * By default, merchant backend serves the html content - * from the /webui root. This should cover most of the - * cases and the rootPath will be the merchant backend - * URL where the instances are - */ - return rootPath.replace("/webui/", ""); -}; - -const loginTokenCodec = buildCodecForObject<LoginToken>() - .property("token", codecForString()) - .property("expiration", codecForTimestamp) - .build("loginToken") -const TOKENS_KEY = buildStorageKey("merchant-token", codecForMap(loginTokenCodec)); - - -export function useBackendURL( - url?: string, -): [string, StateUpdater<string>] { - const [value, setter] = useSimpleLocalStorage( - "merchant-base-url", - url || calculateRootPath(), - ); - - const checkedSetter = (v: ValueOrFunction<string>) => { - return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); - }; - - return [value!, checkedSetter]; -} - -export function useBackendDefaultToken( -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - - const tokenOfDefaultInstance = tokenMap["default"] - const clearCache = useMatchMutate() - useEffect(() => { - clearCache() - }, [tokenOfDefaultInstance]) - - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, "default": value } - setToken(res) - } - } - return [tokenMap["default"], updateToken]; -} - -export function useBackendInstanceToken( - id: string, -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - const [defaultToken, defaultSetToken] = useBackendDefaultToken(); - - // instance named 'default' use the default token - if (id === "default") { - return [defaultToken, defaultSetToken]; - } - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, [id]: value } - setToken(res) - } - } - - return [tokenMap[id], updateToken]; -} - -export function useLang(initial?: string): [string, StateUpdater<string>] { - const browserLang = - typeof window !== "undefined" - ? navigator.language || (navigator as any).userLanguage - : undefined; - const defaultLang = (browserLang || initial || "en").substring(0, 2); - return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>]; -} - -export function useSimpleLocalStorage( - key: string, - initialValue?: string, -): [string | undefined, StateUpdater<string | undefined>] { - const [storedValue, setStoredValue] = useState<string | undefined>( - (): string | undefined => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }, - ); - - const setValue = ( - value?: string | ((val?: string) => string | undefined), - ) => { - setStoredValue((p) => { - const toStore = value instanceof Function ? value(p) : value; - if (typeof window !== "undefined") { - if (!toStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, toStore); - } - } - return toStore; - }); - }; - - return [storedValue, setValue]; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts index ee1576764..f409592b0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,15 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { AccessToken, MerchantBackend } from "../declaration.js"; import { - useAdminAPI, useBackendInstances, - useInstanceAPI, useInstanceDetails, - useManagementAPI, } from "./instance.js"; import { ApiMockEnvironment } from "./testing.js"; import { @@ -35,7 +33,6 @@ import { API_DELETE_INSTANCE, API_GET_CURRENT_INSTANCE, API_LIST_INSTANCES, - API_NEW_LOGIN, API_UPDATE_CURRENT_INSTANCE, API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_BY_ID, @@ -48,55 +45,56 @@ describe("instance api interaction with details", () => { env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + // const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, + } as TalerMerchantApi.InstanceReconfigurationMessage, }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "other_name", - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.updateInstance({ + api.instance.updateCurrentInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage); + } as TalerMerchantApi.InstanceReconfigurationMessage); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "other_name", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "other_name", + // }); }, ], env.buildTestingContext(), @@ -109,56 +107,56 @@ describe("instance api interaction with details", () => { it("should evict cache when setting the instance's token", async () => { const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); + // env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + // response: { + // name: "instance_name", + // auth: { + // method: "token", + // // token: "not-secret", + // }, + // } as TalerMerchantApi.QueryInstancesResponse, + // }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // }, + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { request: { method: "token", token: "secret", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "secret", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); + } as TalerMerchantApi.InstanceAuthConfigurationMessage, + }); + // env.addRequestExpectation(API_NEW_LOGIN, { + // auth: "secret", + // request: { + // scope: "write", + // duration: { + // "d_us": "forever", + // }, + // refreshable: true, + // }, + // }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "instance_name", @@ -166,24 +164,24 @@ describe("instance api interaction with details", () => { method: "token", // token: "secret", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.setNewAccessToken(undefined, "secret" as AccessToken); + // api.setNewAccessToken(undefined, "secret" as AccessToken); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // // token: "secret", + // }, + // }); }, ], env.buildTestingContext(), @@ -202,38 +200,38 @@ describe("instance api interaction with details", () => { method: "token", // token: "not-secret", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // // token: "not-secret", + // }, + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { request: { method: "external", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, + } as TalerMerchantApi.InstanceAuthConfigurationMessage, }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { @@ -241,24 +239,26 @@ describe("instance api interaction with details", () => { auth: { method: "external", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.clearAccessToken(undefined); + api.instance.updateCurrentInstanceAuthentication(undefined, { + method: "external" + }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "external", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "external", + // }, + // }); }, ], env.buildTestingContext(), @@ -331,76 +331,76 @@ describe("instance admin api interaction with listing", () => { instances: [ { name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // name: "instance_name", + // }, + // ], + // }); env.addRequestExpectation(API_CREATE_INSTANCE, { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage, + } as TalerMerchantApi.InstanceConfigurationMessage, }); env.addRequestExpectation(API_LIST_INSTANCES, { response: { instances: [ { name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { name: "other_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.createInstance({ + api.instance.createInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); + } as TalerMerchantApi.InstanceConfigurationMessage) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - { - name: "other_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // name: "instance_name", + // }, + // { + // name: "other_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -418,45 +418,45 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { id: "the_id", name: "second_instance", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // { + // id: "the_id", + // name: "second_instance", + // }, + // ], + // }); env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {}); env.addRequestExpectation(API_LIST_INSTANCES, { @@ -465,28 +465,28 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.deleteInstance("the_id"); + api.instance.deleteInstance(undefined, "the_id"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -542,7 +542,7 @@ describe("instance admin api interaction with listing", () => { // instances: [{ // id: 'default', // name: 'instance_name' - // } as MerchantBackend.Instances.Instance] + // } as TalerMerchantApi.Instance] // }, // }); @@ -572,45 +572,45 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { id: "the_id", name: "second_instance", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // { + // id: "the_id", + // name: "second_instance", + // }, + // ], + // }); env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), { qparam: { @@ -623,28 +623,28 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.purgeInstance("the_id"); + api.instance.deleteInstance(undefined, "the_id", { purge: true }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -664,42 +664,42 @@ describe("instance management api interaction with listing", () => { { id: "managed", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useManagementAPI("managed"); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "managed", + // name: "instance_name", + // }, + // ], + // }); env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, + } as TalerMerchantApi.InstanceReconfigurationMessage, }); env.addRequestExpectation(API_LIST_INSTANCES, { response: { @@ -707,30 +707,30 @@ describe("instance management api interaction with listing", () => { { id: "managed", name: "other_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.updateInstance({ + api.instance.updateCurrentInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); + } as TalerMerchantApi.InstanceConfigurationMessage); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "other_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "managed", + // name: "other_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index 0677191db..f5f8893cd 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,301 +13,112 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useBackendContext } from "../context/backend.js"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useBackendBaseRequest, - useBackendInstanceRequest, - useCredentialsChecker, - useMatchMutate, -} from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; +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; -interface InstanceAPI { - updateInstance: ( - data: MerchantBackend.Instances.InstanceReconfigurationMessage, - ) => Promise<void>; - deleteInstance: () => Promise<void>; - clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>; - setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>; -} - -export function useAdminAPI(): AdminAPI { - const { request } = useBackendBaseRequest(); - const mutateAll = useMatchMutate(); - - const createInstance = async ( - instance: MerchantBackend.Instances.InstanceConfigurationMessage, - ): Promise<void> => { - await request(`/management/instances`, { - method: "POST", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - const purgeInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - params: { - purge: "YES", - }, - }); - - mutateAll(/\/management\/instances/); - }; - - return { createInstance, deleteInstance, purgeInstance }; +export function revalidateInstanceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails", + undefined, + { revalidate: true }, + ); } +export function useInstanceDetails() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export interface AdminAPI { - createInstance: ( - data: MerchantBackend.Instances.InstanceConfigurationMessage, - ) => Promise<void>; - deleteInstance: (id: string) => Promise<void>; - purgeInstance: (id: string) => Promise<void>; -} - -export function useManagementAPI(instanceId: string): InstanceAPI { - const mutateAll = useMatchMutate(); - const { url: backendURL } = useBackendContext() - const { updateToken } = useBackendContext(); - const { request } = useBackendBaseRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "PATCH", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutateAll(/\/management\/instances/); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } + async function fetcher([token]: [AccessToken]) { + return await instance.getCurrentInstanceDetails(token); + } - mutateAll(/\/management\/instances/); - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">, + TalerHttpError + >([session.token, "getCurrentInstanceDetails"], fetcher); - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; + if (data) return data; + if (error) return error; + return undefined; } -export function useInstanceAPI(): InstanceAPI { - const { mutate } = useSWRConfig(); - const { url: backendURL, updateToken } = useBackendContext() - - const { - token: adminToken, - } = useBackendContext(); - const { request } = useBackendInstanceRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/private/`, { - method: "PATCH", - data: instance, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/private/`, { - method: "DELETE", - // token: adminToken, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); +export function revalidateInstanceKYCDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus", + undefined, + { revalidate: true }, + ); +} +export function useInstanceKYCDetails() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - mutate([`/private/`], null); - }; + async function fetcher([token]: [AccessToken]) { + return await instance.getCurrentIntanceKycStatus(token, {}); + } - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getCurrentIntanceKycStatus">, + TalerHttpError + >([session.token, "getCurrentIntanceKycStatus"], fetcher); - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } + if (data) return data; + if (error) return error; + return undefined; - mutate([`/private/`], null); - }; - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; } -export function useInstanceDetails(): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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 }; +export function revalidateManagedInstanceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getInstanceDetails", + undefined, + { revalidate: true }, + ); } +export function useManagedInstanceDetails(instanceId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -type KYCStatus = - | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; - -export function useInstanceKYCDetails(): HttpResponse< - KYCStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([token, instanceId]: [AccessToken, string]) { + return await instance.getInstanceDetails(token, instanceId); + } const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/kyc`], fetcher, { - refreshInterval: 60 * 1000, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateIfStale: false, - revalidateOnMount: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); + TalerMerchantManagementResultByMethod<"getInstanceDetails">, + TalerHttpError + >([session.token, instanceId, "getInstanceDetails"], fetcher); - 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 }; + if (data) return data; + if (error) return error; + return undefined; } -export function useManagedInstanceDetails( - instanceId: string, -): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/management/instances/${instanceId}`], request, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: 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 }; +export function revalidateBackendInstances() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listInstances", + undefined, + { revalidate: true }, + ); } +export function useBackendInstances() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useBackendInstances(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); + async function fetcher([token]: [AccessToken]) { + return await instance.listInstances(token); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >(["/management/instances"], request); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listInstances">, + TalerHttpError + >([session.token, "listInstances"], 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; } diff --git a/packages/merchant-backoffice-ui/src/hooks/listener.ts b/packages/merchant-backoffice-ui/src/hooks/listener.ts index d101f7bb8..f59794fd4 100644 --- a/packages/merchant-backoffice-ui/src/hooks/listener.ts +++ b/packages/merchant-backoffice-ui/src/hooks/listener.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/hooks/notifications.ts b/packages/merchant-backoffice-ui/src/hooks/notifications.ts index 133ddd80b..137ef5333 100644 --- a/packages/merchant-backoffice-ui/src/hooks/notifications.ts +++ b/packages/merchant-backoffice-ui/src/hooks/notifications.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts index c243309a8..9c1eaccbb 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AbsoluteTime, AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; +import { useInstanceOrders, useOrderDetails } from "./order.js"; import { ApiMockEnvironment } from "./testing.js"; import { API_CREATE_ORDER, @@ -32,6 +32,7 @@ import { API_LIST_ORDERS, API_REFUND_ORDER_BY_ID, } from "./urls.js"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("order api interaction with listing", () => { it("should evict cache when creating an order", async () => { @@ -40,39 +41,40 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + orders: [{ order_id: "1" }, { order_id: "2" } as TalerMerchantApi.OrderHistoryEntry], }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ - ({ query, api }) => { - expect(query.loading).true; + ({ query }) => { + expect(query).undefined; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); env.addRequestExpectation(API_CREATE_ORDER, { request: { - order: { amount: "ARS:12", summary: "pay me" }, + order: { amount: "ARS:12" as AmountString, summary: "pay me" }, + lock_uuids: [] }, response: { order_id: "3" }, }); @@ -84,21 +86,21 @@ describe("order api interaction with listing", () => { }, }); - api.createOrder({ - order: { amount: "ARS:12", summary: "pay me" }, - } as any); + api.instance.createOrder(undefined, { + order: { amount: "ARS:12" as AmountString, summary: "pay me" }, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], + // }); }, ], env.buildTestingContext(), @@ -112,45 +114,47 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, - response: { orders: [{ - order_id: "1", - amount: "EUR:12", - refundable: true, - } as MerchantBackend.Orders.OrderHistoryEntry] }, + response: { + orders: [{ + order_id: "1", + amount: "EUR:12", + refundable: true, + } as TalerMerchantApi.OrderHistoryEntry] + }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: true, - }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { + // order_id: "1", + // amount: "EUR:12", + // refundable: true, + // }, + // ], + // }); env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { request: { reason: "double pay", @@ -160,33 +164,35 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, - response: { orders: [ - { order_id: "1", amount: "EUR:12", refundable: false } as any, - ] }, + response: { + orders: [ + { order_id: "1", amount: "EUR:12", refundable: false } as any, + ] + }, }); - api.refundOrder("1", { + api.instance.addRefund(undefined, "1", { reason: "double pay", - refund: "EUR:1", - }); + refund: "EUR:1" as AmountString, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: false, - }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { + // order_id: "1", + // amount: "EUR:12", + // refundable: false, + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -202,35 +208,35 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + orders: [{ order_id: "1" }, { order_id: "2" } as TalerMerchantApi.OrderHistoryEntry], }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); env.addRequestExpectation(API_DELETE_ORDER("1"), {}); @@ -241,18 +247,18 @@ describe("order api interaction with listing", () => { }, }); - api.deleteOrder("1"); + api.instance.deleteOrder(undefined, "1") }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "2" }], + // }); }, ], env.buildTestingContext(), @@ -271,35 +277,31 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - const newDate = (d: Date) => { - //console.log("new date", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useOrderDetails("1"); - const api = useOrderAPI(); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:0", + // }); env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { request: { reason: "double pay", @@ -311,25 +313,25 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:1", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.refundOrder("1", { + api.instance.addRefund(undefined, "1", { reason: "double pay", - refund: "EUR:1", - }); + refund: "EUR:1" as AmountString, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:1", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:1", + // }); }, ], env.buildTestingContext(), @@ -347,35 +349,31 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - const newDate = (d: Date) => { - //console.log("new date", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useOrderDetails("1"); - const api = useOrderAPI(); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:0", + // }); env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), { request: { fields: ["$.summary"], @@ -385,23 +383,23 @@ describe("order api interaction with details", () => { env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { response: { summary: undefined, - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.forgetOrder("1", { + api.instance.forgetOrder(undefined, "1", { fields: ["$.summary"], - }); + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: undefined, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: undefined, + // }); }, ], env.buildTestingContext(), @@ -428,38 +426,35 @@ describe("order listing pagination", () => { }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); + // expect(query.isReachingEnd).true; + // expect(query.isReachingStart).true; - // should not trigger new state update or query - query.loadMore(); - query.loadMorePrev(); }, ], env.buildTestingContext(), @@ -469,7 +464,7 @@ describe("order listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGE_SIZE", async () => { + it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { const env = new ApiMockEnvironment(); const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -478,7 +473,6 @@ describe("order listing pagination", () => { const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20), })); - const ordersFrom20to0 = [...ordersFrom0to20].reverse(); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 20, wired: "yes", date_s: 12 }, @@ -494,34 +488,34 @@ describe("order listing pagination", () => { }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [...ordersFrom20to0, ...ordersFrom20to40], - }); - expect(query.isReachingEnd).false; - expect(query.isReachingStart).false; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [...ordersFrom20to0, ...ordersFrom20to40], + // }); + // expect(query.isReachingEnd).false; + // expect(query.isReachingStart).false; env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -40, wired: "yes", date_s: 13 }, @@ -530,25 +524,25 @@ describe("order listing pagination", () => { }, }); - query.loadMore(); + // query.loadMore(); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // ...ordersFrom20to0, + // ...ordersFrom20to40, + // { order_id: "41" }, + // ], + // }); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 40, wired: "yes", date_s: 12 }, @@ -557,26 +551,26 @@ describe("order listing pagination", () => { }, }); - query.loadMorePrev(); + // query.loadMorePrev(); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { order_id: "-1" }, - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { order_id: "-1" }, + // ...ordersFrom20to0, + // ...ordersFrom20to40, + // { order_id: "41" }, + // ], + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts index e7a893f2c..d0513dc40 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,277 +13,86 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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: MerchantBackend.Orders.PostOrderRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>; - forgetOrder: ( - id: string, - data: MerchantBackend.Orders.ForgetRequest, - ) => Promise<HttpResponseOk<void>>; - refundOrder: ( - id: string, - data: MerchantBackend.Orders.RefundRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.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: MerchantBackend.Orders.PostOrderRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => { - const res = await request<MerchantBackend.Orders.PostOrderResponse>( - `/private/orders`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/orders.*/); - // mutate('') - return res; - }; - const refundOrder = async ( - orderId: string, - data: MerchantBackend.Orders.RefundRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<MerchantBackend.Orders.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: MerchantBackend.Orders.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<MerchantBackend.Orders.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: { instance } } = useSessionContext(); -export function useOrderDetails( - oderId: string, -): HttpResponse< - MerchantBackend.Orders.MerchantOrderStatusResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([dId, token]: [string, AccessToken]) { + return await instance.getOrderDetails(token, dId); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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 revalidateInstanceOrders() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listOrders", + undefined, + { revalidate: true }, + ); +} export function useInstanceOrders( args?: InstanceOrderFilter, - updateFilter?: (d: Date) => void, -): HttpResponsePaginated< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail -> { - 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<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - totalBefore, - ], - orderFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - -totalAfter, - ], - orderFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ 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: { instance } } = useSessionContext(); + + // 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 instance.listOrders(token, { + limit: PAGINATED_LIST_REQUEST, + 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 b045e365a..41ed89f70 100644 --- a/packages/merchant-backoffice-ui/src/hooks/otp.ts +++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,211 +13,68 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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"; const useSWR = _useSWR as unknown as SWRHook; -const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { - "1": { - otp_device_description: "first device", - otp_algorithm: 1, - otp_device_id: "1", - otp_key: "123", - }, - "2": { - otp_device_description: "second device", - otp_algorithm: 0, - otp_device_id: "2", - otp_key: "456", - } +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: { instance } } = useSessionContext(); -export function useOtpDeviceAPI(): OtpDeviceAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); + // const [offset, setOffset] = useState<string | undefined>(); - const createOtpDevice = async ( - data: MerchantBackend.OTP.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, + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listOtpDevices(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; + } - const updateOtpDevice = async ( - deviceId: string, - data: MerchantBackend.OTP.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 { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listOtpDevices">, + TalerHttpError + >([session.token, "offset", "listOtpDevices"], fetcher); - 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; - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - return { - createOtpDevice, - updateOtpDevice, - deleteOtpDevice, - }; + // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id) + return data; } -export interface OtpDeviceAPI { - createOtpDevice: ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateOtpDevice: ( - id: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; +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: { instance } } = useSessionContext(); -export interface InstanceOtpDeviceFilter { -} - -export function useInstanceOtpDevices( - args?: InstanceOtpDeviceFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail -> { - // 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<MerchantBackend.OTP.OtpDeviceSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ 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.otp_devices.length < totalAfter; - const isReachingStart = true; - - 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: () => { - }, - }; - - 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 }; + async function fetcher([dId, token]: [string, AccessToken]) { + return await instance.getOtpDeviceDetails(token, dId); } - return { loading: true }; -} -export function useOtpDeviceDetails( - deviceId: string, -): HttpResponse< - MerchantBackend.OTP.OtpDeviceDetails, - MerchantBackend.ErrorDetail -> { - // 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(); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">, + TalerHttpError + >([deviceId, session.token, "getOtpDeviceDetails"], fetcher); - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices/${deviceId}`], 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 (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts new file mode 100644 index 000000000..a21d2921c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts @@ -0,0 +1,89 @@ +/* + 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/> + */ + +import { + AbsoluteTime, + Codec, + buildCodecForObject, + codecForAbsoluteTime, + codecForBoolean, + codecForConstString, + codecForEither, +} from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; + +export interface Preferences { + advanceOrderMode: boolean; + hideKycUntil: AbsoluteTime; + hideMissingAccountUntil: AbsoluteTime; + dateFormat: "ymd" | "dmy" | "mdy"; +} + +const defaultSettings: Preferences = { + advanceOrderMode: false, + hideKycUntil: AbsoluteTime.never(), + hideMissingAccountUntil: AbsoluteTime.never(), + dateFormat: "ymd", +}; + +export const codecForPreferences = (): Codec<Preferences> => + buildCodecForObject<Preferences>() + .property("advanceOrderMode", codecForBoolean()) + .property("hideKycUntil", codecForAbsoluteTime) + .property("hideMissingAccountUntil", codecForAbsoluteTime) + .property( + "dateFormat", + codecForEither( + codecForConstString("ymd"), + codecForConstString("dmy"), + codecForConstString("mdy"), + ), + ) + .build("Preferences"); + +const PREFERENCES_KEY = buildStorageKey( + "merchant-preferences", + codecForPreferences(), +); + +export function usePreference(): [ + Readonly<Preferences>, + <T extends keyof Preferences>(key: T, value: Preferences[T]) => void, + (s: Preferences) => void, +] { + const { value, update } = useLocalStorage(PREFERENCES_KEY, defaultSettings); + function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + + return [value, updateField, update]; +} + +export function dateFormatForSettings(s: Preferences): string { + switch (s.dateFormat) { + case "ymd": + return "yyyy/MM/dd"; + case "dmy": + return "dd/MM/yyyy"; + case "mdy": + return "MM/dd/yyyy"; + } +} + +export function datetimeFormatForSettings(s: Preferences): string { + return dateFormatForSettings(s) + " HH:mm:ss"; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts index 7cac10e25..39281241c 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -21,10 +21,8 @@ import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; import { useInstanceProducts, - useProductAPI, useProductDetails, } from "./product.js"; import { ApiMockEnvironment } from "./testing.js"; @@ -35,6 +33,8 @@ import { API_LIST_PRODUCTS, API_UPDATE_PRODUCT_BY_ID, } from "./urls.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("product api interaction with listing", () => { it("should evict cache when creating a product", async () => { @@ -42,64 +42,64 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); env.addRequestExpectation(API_CREATE_PRODUCT, { request: { price: "ARS:23", - } as MerchantBackend.Products.ProductAddDetail, + } as TalerMerchantApi.ProductAddDetail, }); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], + products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { response: { price: "ARS:23", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.createProduct({ + api.instance.addProduct(undefined, { price: "ARS:23", } as any); }, @@ -107,25 +107,25 @@ describe("product api interaction with listing", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:12", - }, - { - id: "2345", - price: "ARS:23", - }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { + // id: "1234", + // price: "ARS:12", + // }, + // { + // id: "2345", + // price: "ARS:23", + // }, + // ]); }, ], env.buildTestingContext(), @@ -140,54 +140,54 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { request: { price: "ARS:13", - } as MerchantBackend.Products.ProductPatchDetail, + } as TalerMerchantApi.ProductPatchDetail, }); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:13", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.updateProduct("1234", { + api.instance.updateProduct(undefined, "1234", { price: "ARS:13", } as any); }, @@ -195,15 +195,15 @@ describe("product api interaction with listing", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:13", - }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { + // id: "1234", + // price: "ARS:13", + // }, + // ]); }, ], env.buildTestingContext(), @@ -218,71 +218,71 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], + products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:23" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { id: "1234", price: "ARS:12" }, - { id: "2345", price: "ARS:23" }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { id: "1234", price: "ARS:12" }, + // { id: "2345", price: "ARS:23" }, + // ]); env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.deleteProduct("2345"); + api.instance.deleteProduct(undefined, "2345"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); }, ], env.buildTestingContext(), @@ -300,44 +300,44 @@ describe("product api interaction with details", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "this is a description", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useProductDetails("12"); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "this is a description", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // description: "this is a description", + // }); env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { request: { description: "other description", - } as MerchantBackend.Products.ProductPatchDetail, + } as TalerMerchantApi.ProductPatchDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "other description", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.updateProduct("12", { + api.instance.updateProduct(undefined, "12", { description: "other description", } as any); }, @@ -345,12 +345,12 @@ describe("product api interaction with details", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "other description", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // description: "other description", + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts index b54cd4d91..defda5552 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,165 +13,91 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend, WithId } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; +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 { PAGINATED_LIST_REQUEST } 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: MerchantBackend.Products.ProductAddDetail, - ) => Promise<void>; - updateProduct: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - deleteProduct: (id: string) => Promise<void>; - lockProduct: ( - id: string, - data: MerchantBackend.Products.LockRequest, - ) => Promise<void>; +type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number }; +function notUndefined(c: ProductWithId | undefined): c is ProductWithId { + return c !== undefined; } -export function useProductAPI(): ProductAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); - - const createProduct = async ( - data: MerchantBackend.Products.ProductAddDetail, - ): Promise<void> => { - const res = await request(`/private/products`, { - method: "POST", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const updateProduct = async ( - productId: string, - data: MerchantBackend.Products.ProductPatchDetail, - ): Promise<void> => { - const r = await request(`/private/products/${productId}`, { - method: "PATCH", - data, - }); +export function revalidateInstanceProducts() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listProductsWithId", + undefined, + { revalidate: true }, + ); +} +export function useInstanceProducts() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - return await mutateAll(/.*\/private\/products.*/); - }; + const [offset, setOffset] = useState<number | undefined>(); - const deleteProduct = async (productId: string): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "DELETE", - }); - await mutate([`/private/products`]); - }; - - const lockProduct = async ( - productId: string, - data: MerchantBackend.Products.LockRequest, - ): Promise<void> => { - await request(`/private/products/${productId}/lock`, { - method: "POST", - data, + async function fetcher([token, bid]: [AccessToken, number]) { + const list = await instance.listProducts(token, { + limit: PAGINATED_LIST_REQUEST, + offset: bid === undefined ? undefined: String(bid), + order: "dec", }); + if (list.type !== "ok") { + return list; + } + const all: Array<ProductWithId | undefined> = await Promise.all( + list.body.products.map(async (c) => { + const r = await instance.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 await mutateAll(/.*"\/private\/products.*/); - }; - - const getProduct = async ( - productId: string, - ): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "GET", - }); + const { data, error } = useSWR< + OperationOk<{ products: ProductWithId[] }> | + TalerMerchantManagementErrorsByMethod<"listProducts">, + TalerHttpError + >([session.token, offset, "listProductsWithId"], fetcher); - return - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; + return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial) } -export function useInstanceProducts(): HttpResponse< - (MerchantBackend.Products.ProductDetail & WithId)[], - MerchantBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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}`, +export function revalidateProductDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getProductDetails", + undefined, + { revalidate: true }, ); - const { data: products, error: productError } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>[], - RequestError<MerchantBackend.ErrorDetail> - >([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\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; - } - return { loading: true }; } +export function useProductDetails(productId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + async function fetcher([pid, token]: [string, AccessToken]) { + return await instance.getProductDetails(token, pid); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getProductDetails">, + TalerHttpError + >([productId, session.token, "getProductDetails"], fetcher); -export function useProductDetails( - productId: string, -): HttpResponse< - MerchantBackend.Products.ProductDetail, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts deleted file mode 100644 index b3eecd754..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceReserves, - useReserveDetails, - useReservesAPI, - useRewardDetails, -} from "./reserves.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_AUTHORIZE_REWARD, - API_AUTHORIZE_REWARD_FOR_RESERVE, - API_CREATE_RESERVE, - API_DELETE_RESERVE, - API_GET_RESERVE_BY_ID, - API_GET_REWARD_BY_ID, - API_LIST_RESERVES, -} from "./urls.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -describe("reserve api interaction with listing", () => { - it("should evict cache when creating a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "11" }], - }); - - env.addRequestExpectation(API_CREATE_RESERVE, { - request: { - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }, - response: { - reserve_pub: "22", - accounts: [], - }, - }); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.createReserve({ - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [ - { reserve_pub: "11" }, - { reserve_pub: "22" }, - { reserve_pub: "33" }, - ], - }); - - env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.deleteReserve("11"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with details", () => { - it("should evict cache when adding a reward for a specific reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeRewardReserve("11", { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when adding a reward for a random reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD, { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeReward({ - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with reward details", () => { - it("should list rewards", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { - response: { - total_picked_up: "USD:12", - reason: "not", - } as MerchantBackend.Rewards.RewardDetails, - qparam: { - pickups: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useRewardDetails("11"); - return { query }; - }, - {}, - [ - ({ query }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - total_picked_up: "USD:12", - reason: "not", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts deleted file mode 100644 index b719bfbe6..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useReservesAPI(): ReserveMutateAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - const { request } = useBackendInstanceRequest(); - - const createReserve = async ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ): Promise< - HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> - > => { - const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( - `/private/reserves`, - { - method: "POST", - data, - }, - ); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - const authorizeRewardReserve = async ( - pub: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/reserves/${pub}/authorize-reward`, - { - method: "POST", - data, - }, - ); - - //evict reserve details query - await mutate([`/private/reserves/${pub}`]); - - return res; - }; - - const authorizeReward = async ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/rewards`, - { - method: "POST", - data, - }, - ); - - //evict all details query - await mutateAll(/.*private\/reserves\/.*/); - - return res; - }; - - const deleteReserve = async ( - pub: string, - ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => { - const res = await request<void>(`/private/reserves/${pub}`, { - method: "DELETE", - }); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; -} - -export interface ReserveMutateAPI { - createReserve: ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; - authorizeRewardReserve: ( - id: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - authorizeReward: ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - deleteReserve: ( - id: string, - ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; -} - -export function useInstanceReserves(): HttpResponse< - MerchantBackend.Rewards.RewardReserveStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves`], fetcher); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useReserveDetails( - reserveId: string, -): HttpResponse< - MerchantBackend.Rewards.ReserveDetail, - MerchantBackend.ErrorDetail -> { - const { reserveDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { - 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 }; -} - -export function useRewardDetails( - rewardId: string, -): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { - const { rewardsDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { - 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 }; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index ee8728cc8..500a94a48 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,254 +13,76 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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: MerchantBackend.Template.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: MerchantBackend.Template.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: MerchantBackend.Template.UsingTemplateDetails, - ): Promise< - HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse> - > => { - const res = await request<MerchantBackend.Template.UsingTemplateResponse>( - `/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: MerchantBackend.Template.TemplateAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateTemplate: ( - id: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ) => Promise<HttpResponseOk<void>>; - testTemplateExist: ( - id: string - ) => Promise<HttpResponseOk<void>>; - deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; - createOrderFromTemplate: ( - id: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>; -} export interface InstanceTemplateFilter { - //FIXME: add filter to the template list - position?: string; } -export function useInstanceTemplates( - args?: InstanceTemplateFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail -> { - 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; +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: { instance } } = useSessionContext(); - /** - * 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<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail>>( - [ - `/private/templates`, - args?.position, - totalBefore, - ], - templateFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates`, args?.position, -totalAfter], templateFetcher); + const [offset, setOffset] = useState<string | undefined>(); - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); + async function fetcher([token, bid]: [AccessToken, string]) { + return await instance.listTemplates(token, { + limit: PAGINATED_LIST_REQUEST, + offset: bid, + order: "dec", + }); + } - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ 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) + return data; - 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 revalidateTemplateDetails() { + 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: { instance } } = useSessionContext(); -export function useTemplateDetails( - templateId: string, -): HttpResponse< - MerchantBackend.Template.TemplateDetails, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); + async function fetcher([tid, token]: [string, AccessToken]) { + return await instance.getTemplateDetails(token, tid); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx index 3ea22475b..fc78f6c58 100644 --- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx +++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -24,10 +24,22 @@ import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; import { SWRConfig } from "swr"; import { ApiContextProvider } from "@gnu-taler/web-util/browser"; -import { BackendContextProvider } from "../context/backend.js"; -import { InstanceContextProvider } from "../context/instance.js"; -import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser"; -import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient } from "@gnu-taler/taler-util"; +import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util"; + +interface RequestOptions { + method?: "GET" | "POST" | "HEAD", + params?: any, + token?: string | undefined, + data?: any, +} +interface HttpResponseOk<T> { + ok: true, + data: T, + loading: boolean, + clientError: boolean, + serverError: boolean, + info: any, +} export class ApiMockEnvironment extends MockEnvironment { constructor(debug = false) { @@ -144,21 +156,21 @@ export class ApiMockEnvironment extends MockEnvironment { } const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient) - const bankIntegration = bankCore.getIntegrationAPI() - const bankRevenue = bankCore.getRevenueAPI("a") - const bankWire = bankCore.getWireGatewayAPI("b") + const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient) + const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient) + const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient) return ( - <BackendContextProvider defaultUrl="http://backend"> - <InstanceContextProvider - value={{ - token: undefined, - id: "default", - admin: true, - changeToken: () => null, - }} - > - <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}> + // <BackendContextProvider defaultUrl="http://backend"> + // <InstanceContextProvider + // value={{ + // token: undefined, + // id: "default", + // admin: true, + // changeToken: () => null, + // }} + // > + <ApiContextProvider value={{ request : undefined as any, bankCore, bankIntegration, bankRevenue, bankWire }}> <SC value={{ loadingTimeout: 0, @@ -172,8 +184,8 @@ export class ApiMockEnvironment extends MockEnvironment { {children} </SC> </ApiContextProvider> - </InstanceContextProvider> - </BackendContextProvider> + // </InstanceContextProvider> + // </BackendContextProvider> ); }; } diff --git a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts index 0266fe536..62f364972 100644 --- a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts +++ b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts @@ -13,114 +13,66 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; import { MerchantBackend } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useSessionContext } from "../context/session.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; const useSWR = _useSWR as unknown as SWRHook; -export interface TokenFamilyAPI { - createTokenFamily: ( - data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail, - ) => Promise<void>; - updateTokenFamily: ( - slug: string, - data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, - ) => Promise<void>; - deleteTokenFamily: (slug: string) => Promise<void>; -} - -export function useTokenFamilyAPI(): TokenFamilyAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); +export function useInstanceTokenFamilies() { + const { state: session, lib: { instance } } = useSessionContext(); - const createTokenFamily = async ( - data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail, - ): Promise<void> => { - const res = await request(`/private/tokenfamilies`, { - method: "POST", - data, - }); + // const [offset, setOffset] = useState<number | undefined>(); - return await mutateAll(/.*"\/private\/tokenfamilies.*/); - }; - - const updateTokenFamily = async ( - tokenFamilySlug: string, - data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, - ): Promise<void> => { - const r = await request(`/private/tokenfamilies/${tokenFamilySlug}`, { - method: "PATCH", - data, + async function fetcher([token, bid]: [AccessToken, number]) { + return await instance.listTokenFamilies(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid === undefined ? undefined: String(bid), + // order: "dec", }); + } - return await mutateAll(/.*"\/private\/tokenfamilies.*/); - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listTokenFamilies">, + TalerHttpError + >([session.token, "offset", "listTokenFamilies"], fetcher); - const deleteTokenFamily = async (tokenFamilySlug: string): Promise<void> => { - await request(`/private/tokenfamilies/${tokenFamilySlug}`, { - method: "DELETE", - }); - await mutate([`/private/tokenfamilies`]); - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - return { createTokenFamily, updateTokenFamily, deleteTokenFamily }; + return data; } -export function useInstanceTokenFamilies(): HttpResponse< - (MerchantBackend.TokenFamilies.TokenFamilyEntry)[], - MerchantBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<MerchantBackend.TokenFamilies.TokenFamilySummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/tokenfamilies`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - - if (list) { - return { ok: true, data: list.data.token_families }; +export function useTokenFamilyDetails(tokenFamilySlug: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + async function fetcher([slug, token]: [string, AccessToken]) { + return await instance.getTokenFamilyDetails(token, slug); } - return { loading: true }; + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getTokenFamilyDetails">, + TalerHttpError + >([tokenFamilySlug, session.token, "getTokenFamilyDetails"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return data; } -export function useTokenFamilyDetails( - tokenFamilySlug: string, -): HttpResponse< - MerchantBackend.TokenFamilies.TokenFamilyDetail, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.TokenFamilies.TokenFamilyDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/tokenfamilies/${tokenFamilySlug}`], 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 }; +export interface TokenFamilyAPI { + createTokenFamily: ( + data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail, + ) => Promise<void>; + updateTokenFamily: ( + slug: string, + data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail, + ) => Promise<void>; + deleteTokenFamily: (slug: string) => Promise<void>; } diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts index a7187af27..7daaf5049 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { + AmountString, + PaytoString, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; import { ApiMockEnvironment } from "./testing.js"; -import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; +import { useInstanceTransfers } from "./transfer.js"; +import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("transfer api interaction with listing", () => { it("should evict cache when informing a transfer", async () => { @@ -33,36 +38,36 @@ describe("transfer api interaction with listing", () => { env.addRequestExpectation(API_LIST_TRANSFERS, { qparam: { limit: -20 }, response: { - transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], + transfers: [{ wtid: "2" } as TalerMerchantApi.TransferDetails], }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceTransfers({}, moveCursor); - const api = useTransferAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "2" }], + // }); env.addRequestExpectation(API_INFORM_TRANSFERS, { request: { @@ -81,24 +86,24 @@ describe("transfer api interaction with listing", () => { }, }); - api.informTransfer({ + api.instance.informWireTransfer(undefined, { wtid: "3", - credit_amount: "EUR:1", + credit_amount: "EUR:1" as AmountString, exchange_url: "exchange.url", - payto_uri: "payto://", + payto_uri: "payto://" as PaytoString, }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "3" }, { wtid: "2" }], - }); + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "3" }, { wtid: "2" }], + // }); }, ], env.buildTestingContext(), @@ -120,12 +125,16 @@ describe("transfer listing pagination", () => { }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); + const query = useInstanceTransfers( + { payto_uri: "payto://" }, + moveCursor, + ); + return { query }; }, {}, [ @@ -133,22 +142,18 @@ describe("transfer listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, (query) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }, { wtid: "1" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "2" }, { wtid: "1" }], + // }); + // expect(query.isReachingEnd).true; + // expect(query.isReachingStart).true; - //check that this button won't trigger more updates since - //has reach end and start - query.loadMore(); - query.loadMorePrev(); }, ], env.buildTestingContext(), @@ -158,7 +163,7 @@ describe("transfer listing pagination", () => { expect(hookBehavior).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGE_SIZE", async () => { + it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { const env = new ApiMockEnvironment(); const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -167,7 +172,7 @@ describe("transfer listing pagination", () => { const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i + 20), })); - const transfersFrom20to0 = [...transfersFrom0to20].reverse(); + // const transfersFrom20to0 = [...transfersFrom0to20].reverse(); env.addRequestExpectation(API_LIST_TRANSFERS, { qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, @@ -183,16 +188,17 @@ describe("transfer listing pagination", () => { }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - return useInstanceTransfers( + const query = useInstanceTransfers( { payto_uri: "payto://", position: "1" }, moveCursor, ); + return { query }; }, {}, [ @@ -200,17 +206,17 @@ describe("transfer listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).true; + // expect(result.loading).true; }, (result) => { - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [...transfersFrom20to0, ...transfersFrom20to40], - }); - expect(result.isReachingEnd).false; - expect(result.isReachingStart).false; + // expect(result.loading).undefined; + // expect(result.ok).true; + // if (!result.ok) return; + // expect(result.data).deep.equals({ + // transfers: [...transfersFrom20to0, ...transfersFrom20to40], + // }); + // expect(result.isReachingEnd).false; + // expect(result.isReachingStart).false; //query more env.addRequestExpectation(API_LIST_TRANSFERS, { @@ -219,30 +225,30 @@ describe("transfer listing pagination", () => { transfers: [...transfersFrom20to40, { wtid: "41" }], }, }); - result.loadMore(); + // result.loadMore(); }, (result) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).true; + // expect(result.loading).true; }, (result) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [ - ...transfersFrom20to0, - ...transfersFrom20to40, - { wtid: "41" }, - ], - }); - expect(result.isReachingEnd).true; - expect(result.isReachingStart).false; + // expect(result.loading).undefined; + // expect(result.ok).true; + // if (!result.ok) return; + // expect(result.data).deep.equals({ + // transfers: [ + // ...transfersFrom20to0, + // ...transfersFrom20to40, + // { wtid: "41" }, + // ], + // }); + // expect(result.isReachingEnd).true; + // expect(result.isReachingStart).false; }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts index 27c3bdc75..6f77369c2 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,176 +13,56 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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 useTransferAPI(): TransferAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const informTransfer = async ( - data: MerchantBackend.Transfers.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: MerchantBackend.Transfers.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< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail -> { - 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<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - totalBefore, - ], - transferFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - -totalAfter, - ], - transferFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); + updatePosition: (id: string | undefined) => void = (() => { }), +) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(args?.position); + + async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) { + return await instance.listWireTransfers(token, { + paytoURI: p, + verified: v, + limit: PAGINATED_LIST_REQUEST, + 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/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts index b6485259f..95e1c04f3 100644 --- a/packages/merchant-backoffice-ui/src/hooks/urls.ts +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -18,16 +18,16 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { Query } from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../declaration.js"; //////////////////// // ORDER //////////////////// export const API_CREATE_ORDER: Query< - MerchantBackend.Orders.PostOrderRequest, - MerchantBackend.Orders.PostOrderResponse + TalerMerchantApi.PostOrderRequest, + TalerMerchantApi.PostOrderResponse > = { method: "POST", url: "http://backend/instances/default/private/orders", @@ -35,14 +35,14 @@ export const API_CREATE_ORDER: Query< export const API_GET_ORDER_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ +): Query<unknown, TalerMerchantApi.MerchantOrderStatusResponse> => ({ method: "GET", url: `http://backend/instances/default/private/orders/${id}`, }); export const API_LIST_ORDERS: Query< unknown, - MerchantBackend.Orders.OrderHistory + TalerMerchantApi.OrderHistory > = { method: "GET", url: "http://backend/instances/default/private/orders", @@ -51,8 +51,8 @@ export const API_LIST_ORDERS: Query< export const API_REFUND_ORDER_BY_ID = ( id: string, ): Query< - MerchantBackend.Orders.RefundRequest, - MerchantBackend.Orders.MerchantRefundResponse + TalerMerchantApi.RefundRequest, + TalerMerchantApi.MerchantRefundResponse > => ({ method: "POST", url: `http://backend/instances/default/private/orders/${id}/refund`, @@ -60,14 +60,14 @@ export const API_REFUND_ORDER_BY_ID = ( export const API_FORGET_ORDER_BY_ID = ( id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +): Query<TalerMerchantApi.ForgetRequest, unknown> => ({ method: "PATCH", url: `http://backend/instances/default/private/orders/${id}/forget`, }); export const API_DELETE_ORDER = ( id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +): Query<TalerMerchantApi.ForgetRequest, unknown> => ({ method: "DELETE", url: `http://backend/instances/default/private/orders/${id}`, }); @@ -78,14 +78,14 @@ export const API_DELETE_ORDER = ( export const API_LIST_TRANSFERS: Query< unknown, - MerchantBackend.Transfers.TransferList + TalerMerchantApi.TransferList > = { method: "GET", url: "http://backend/instances/default/private/transfers", }; export const API_INFORM_TRANSFERS: Query< - MerchantBackend.Transfers.TransferInformation, + TalerMerchantApi.TransferInformation, {} > = { method: "POST", @@ -97,7 +97,7 @@ export const API_INFORM_TRANSFERS: Query< //////////////////// export const API_CREATE_PRODUCT: Query< - MerchantBackend.Products.ProductAddDetail, + TalerMerchantApi.ProductAddDetail, unknown > = { method: "POST", @@ -106,7 +106,7 @@ export const API_CREATE_PRODUCT: Query< export const API_LIST_PRODUCTS: Query< unknown, - MerchantBackend.Products.InventorySummaryResponse + TalerMerchantApi.InventorySummaryResponse > = { method: "GET", url: "http://backend/instances/default/private/products", @@ -114,7 +114,7 @@ export const API_LIST_PRODUCTS: Query< export const API_GET_PRODUCT_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ +): Query<unknown, TalerMerchantApi.ProductDetail> => ({ method: "GET", url: `http://backend/instances/default/private/products/${id}`, }); @@ -122,8 +122,8 @@ export const API_GET_PRODUCT_BY_ID = ( export const API_UPDATE_PRODUCT_BY_ID = ( id: string, ): Query< - MerchantBackend.Products.ProductPatchDetail, - MerchantBackend.Products.InventorySummaryResponse + TalerMerchantApi.ProductPatchDetail, + TalerMerchantApi.InventorySummaryResponse > => ({ method: "PATCH", url: `http://backend/instances/default/private/products/${id}`, @@ -135,67 +135,11 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ }); //////////////////// -// RESERVES -//////////////////// - -export const API_CREATE_RESERVE: Query< - MerchantBackend.Rewards.ReserveCreateRequest, - MerchantBackend.Rewards.ReserveCreateConfirmation -> = { - method: "POST", - url: "http://backend/instances/default/private/reserves", -}; -export const API_LIST_RESERVES: Query< - unknown, - MerchantBackend.Rewards.RewardReserveStatus -> = { - method: "GET", - url: "http://backend/instances/default/private/reserves", -}; - -export const API_GET_RESERVE_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/reserves/${pub}`, -}); - -export const API_GET_REWARD_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ - method: "GET", - url: `http://backend/instances/default/private/rewards/${pub}`, -}); - -export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( - pub: string, -): Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> => ({ - method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, -}); - -export const API_AUTHORIZE_REWARD: Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> = { - method: "POST", - url: `http://backend/instances/default/private/rewards`, -}; - -export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/reserves/${id}`, -}); - -//////////////////// // INSTANCE ADMIN //////////////////// export const API_CREATE_INSTANCE: Query< - MerchantBackend.Instances.InstanceConfigurationMessage, + TalerMerchantApi.InstanceConfigurationMessage, unknown > = { method: "POST", @@ -204,21 +148,21 @@ export const API_CREATE_INSTANCE: Query< export const API_GET_INSTANCE_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ +): Query<unknown, TalerMerchantApi.QueryInstancesResponse> => ({ method: "GET", url: `http://backend/management/instances/${id}`, }); export const API_GET_INSTANCE_KYC_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ +): Query<unknown, TalerMerchantApi.AccountKycRedirects> => ({ method: "GET", url: `http://backend/management/instances/${id}/kyc`, }); export const API_LIST_INSTANCES: Query< unknown, - MerchantBackend.Instances.InstancesResponse + TalerMerchantApi.InstancesResponse > = { method: "GET", url: "http://backend/management/instances", @@ -227,7 +171,7 @@ export const API_LIST_INSTANCES: Query< export const API_UPDATE_INSTANCE_BY_ID = ( id: string, ): Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, + TalerMerchantApi.InstanceReconfigurationMessage, unknown > => ({ method: "PATCH", @@ -237,7 +181,7 @@ export const API_UPDATE_INSTANCE_BY_ID = ( export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( id: string, ): Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, + TalerMerchantApi.InstanceAuthConfigurationMessage, unknown > => ({ method: "POST", @@ -250,24 +194,12 @@ export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ }); //////////////////// -// AUTH -//////////////////// - -export const API_NEW_LOGIN: Query< - MerchantBackend.Instances.LoginTokenRequest, - unknown -> = ({ - method: "POST", - url: `http://backend/private/token`, -}); - -//////////////////// // INSTANCE //////////////////// export const API_GET_CURRENT_INSTANCE: Query< unknown, - MerchantBackend.Instances.QueryInstancesResponse + TalerMerchantApi.QueryInstancesResponse > = { method: "GET", url: `http://backend/instances/default/private/`, @@ -275,14 +207,14 @@ export const API_GET_CURRENT_INSTANCE: Query< export const API_GET_CURRENT_INSTANCE_KYC: Query< unknown, - MerchantBackend.KYC.AccountKycRedirects + TalerMerchantApi.AccountKycRedirects > = { method: "GET", url: `http://backend/instances/default/private/kyc`, }; export const API_UPDATE_CURRENT_INSTANCE: Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, + TalerMerchantApi.InstanceReconfigurationMessage, unknown > = { method: "PATCH", @@ -290,7 +222,7 @@ export const API_UPDATE_CURRENT_INSTANCE: Query< }; export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, + TalerMerchantApi.InstanceAuthConfigurationMessage, unknown > = { method: "POST", diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts deleted file mode 100644 index 8c1ebd9f6..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { - Codec, - buildCodecForObject, - codecForBoolean, - codecForConstString, - codecForEither, - codecForString, -} from "@gnu-taler/taler-util"; - -export interface Settings { - advanceOrderMode: boolean; - dateFormat: "ymd" | "dmy" | "mdy"; -} - -const defaultSettings: Settings = { - advanceOrderMode: false, - dateFormat: "ymd", -} - -export const codecForSettings = (): Codec<Settings> => - buildCodecForObject<Settings>() - .property("advanceOrderMode", codecForBoolean()) - .property("dateFormat", codecForEither( - codecForConstString("ymd"), - codecForConstString("dmy"), - codecForConstString("mdy"), - )) - .build("Settings"); - -const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); - -export function useSettings(): [ - Readonly<Settings>, - (s: Settings) => void, -] { - const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); - - // const parsed: Settings = value ?? defaultSettings; - // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { - // const next = { ...parsed, [k]: v } - // update(next); - // } - return [value, update]; -} - -export function dateFormatForSettings(s: Settings): string { - switch (s.dateFormat) { - case "ymd": return "yyyy/MM/dd" - case "dmy": return "dd/MM/yyyy" - case "mdy": return "MM/dd/yyyy" - } -} - -export function datetimeFormatForSettings(s: Settings): string { - return dateFormatForSettings(s) + " HH:mm:ss" -}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts index ad6bf96e2..fe37162aa 100644 --- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts +++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,166 +13,106 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -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(); +export interface InstanceWebhookFilter { +} - const createWebhook = async ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; +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: { instance } } = useSessionContext(); - const updateWebhook = async ( - webhookId: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; + // const [offset, setOffset] = useState<string | undefined>(); - const deleteWebhook = async ( - webhookId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "DELETE", + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listWebhooks(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; + } - return { createWebhook, updateWebhook, deleteWebhook }; -} + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listWebhooks">, + TalerHttpError + >([session.token, "offset", "listWebhooks"], fetcher); -export interface WebhookAPI { - createWebhook: ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateWebhook: ( - id: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; + 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) + return data; } -export interface InstanceWebhookFilter { - //FIXME: add filter to the webhook list - position?: string; +type PaginatedResult<T> = OperationOk<T> & { + isLastPage: boolean; + isFirstPage: boolean; + loadNext(): void; + loadFirst(): void; } -export function useInstanceWebhooks( - args?: InstanceWebhookFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail -> { - 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<MerchantBackend.Webhooks.WebhookSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ 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); - } +//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 < PAGINATED_LIST_REQUEST; + const isFirstPage = offset === undefined; + + const result = structuredClone(data); + if (result.length == PAGINATED_LIST_REQUEST) { + 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: { instance } } = useSessionContext(); + + async function fetcher([hookId, token]: [string, AccessToken]) { + return await instance.getWebhookDetails(token, hookId); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getWebhookDetails">, + TalerHttpError + >([webhookId, session.token, "getWebhookDetails"], fetcher); -export function useWebhookDetails( - webhookId: string, -): HttpResponse< - MerchantBackend.Webhooks.WebhookDetails, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/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; } |