diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/hooks')
10 files changed, 662 insertions, 172 deletions
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index 145a366f6..ecd34df6d 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -33,8 +33,9 @@ import { } from "@gnu-taler/web-util/browser"; import { useApiContext } from "@gnu-taler/web-util/browser"; + export function useMatchMutate(): ( - re: RegExp, + re?: RegExp, value?: unknown, ) => Promise<any> { const { cache, mutate } = useSWRConfig(); @@ -45,13 +46,19 @@ export function useMatchMutate(): ( ); } - return function matchRegexMutate(re: RegExp, value?: unknown) { - const allKeys = Array.from(cache.keys()); - const keys = allKeys.filter((key) => re.test(key)); - const mutations = keys.map((key) => { - return mutate(key, value, true); + 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, }); - return Promise.all(mutations); }; } @@ -106,32 +113,32 @@ interface useBackendInstanceRequestType { ) => Promise<HttpResponseOk<T>>; fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - tipsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>; + rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; orderFetcher: <T>( - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, + params: [endpoint: string, + paid?: YesOrNo, + refunded?: YesOrNo, + wired?: YesOrNo, + searchDate?: Date, + delta?: number,] ) => Promise<HttpResponseOk<T>>; transferFetcher: <T>( - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, + params: [endpoint: string, + payto_uri?: string, + verified?: string, + position?: string, + delta?: number,] ) => Promise<HttpResponseOk<T>>; templateFetcher: <T>( - endpoint: string, - position?: string, - delta?: number, + params: [endpoint: string, + position?: string, + delta?: number] ) => Promise<HttpResponseOk<T>>; webhookFetcher: <T>( - endpoint: string, - position?: string, - delta?: number, + params: [endpoint: string, + position?: string, + delta?: number] ) => Promise<HttpResponseOk<T>>; } interface useBackendBaseRequestType { @@ -147,7 +154,7 @@ export function useCredentialsChecker() { const { request } = useApiContext(); //check against instance details endpoint //while merchant backend doesn't have a login endpoint - return async function testLogin( + async function testLogin( instance: string, token: string, ): Promise<{ @@ -167,6 +174,7 @@ export function useCredentialsChecker() { return { valid: false, cause: ErrorType.UNEXPECTED }; } }; + return testLogin } /** @@ -212,8 +220,9 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const multiFetcher = useCallback( function multiFetcherImpl<T>( - endpoints: string[], + args: [endpoints: string[]], ): Promise<HttpResponseOk<T>[]> { + const [endpoints] = args return Promise.all( endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { token }), @@ -232,13 +241,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const orderFetcher = useCallback( function orderFetcherImpl<T>( - endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number, + 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 ? (searchDate.getTime() / 1000) + 1 @@ -260,7 +270,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { ): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { params: { - tips: "yes", + rewards: "yes", }, token, }); @@ -268,8 +278,8 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { [baseUrl, token], ); - const tipsDetailFetcher = useCallback( - function tipsDetailFetcherImpl<T>( + const rewardsDetailFetcher = useCallback( + function rewardsDetailFetcherImpl<T>( endpoint: string, ): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { @@ -284,12 +294,13 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const transferFetcher = useCallback( function transferFetcherImpl<T>( - endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number, + 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; @@ -305,10 +316,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const templateFetcher = useCallback( function templateFetcherImpl<T>( - endpoint: string, - position?: string, - delta?: number, + args: [endpoint: string, + position?: string, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args const params: any = {}; if (delta !== undefined) { params.limit = delta; @@ -322,10 +334,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const webhookFetcher = useCallback( function webhookFetcherImpl<T>( - endpoint: string, - position?: string, - delta?: number, + args: [endpoint: string, + position?: string, + delta?: number,] ): Promise<HttpResponseOk<T>> { + const [endpoint, position, delta] = args const params: any = {}; if (delta !== undefined) { params.limit = delta; @@ -343,7 +356,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { multiFetcher, orderFetcher, reserveDetailFetcher, - tipsDetailFetcher, + rewardsDetailFetcher, transferFetcher, templateFetcher, webhookFetcher, diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts new file mode 100644 index 000000000..03b064646 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -0,0 +1,217 @@ +/* + 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, + 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"; +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 function useBankAccountAPI(): BankAccountAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + 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 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, + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + const deleteBankAccount = async ( + h_wire: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_ACCOUNTS[h_wire] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/accounts/${h_wire}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/accounts.*/); + return res; + }; + + return { + createBankAccount, + updateBankAccount, + deleteBankAccount, + }; +} + +export interface BankAccountAPI { + createBankAccount: ( + data: MerchantBackend.BankAccounts.AccountAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateBankAccount: ( + id: string, + data: MerchantBackend.BankAccounts.AccountPatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; +} + +export interface InstanceBankAccountFilter { +} + +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 }; + } + 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, 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 }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index b77b9dea8..79b22304a 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -19,9 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { StateUpdater, useCallback, useState } from "preact/hooks"; +import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks"; import { ValueOrFunction } from "../utils/types.js"; import { useMemoryStorage } from "@gnu-taler/web-util/browser"; +import { useMatchMutate } from "./backend.js"; const calculateRootPath = () => { const rootPath = @@ -56,8 +57,22 @@ export function useBackendDefaultToken( ): [string | undefined, ((d: string | undefined) => void)] { // uncomment for testing initialValue = "secret-token:secret" as string | undefined - const { update, value } = useMemoryStorage(`backend-token`, initialValue) - return [value, update]; + const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue) + const clearCache = useMatchMutate() + useEffect(() => { + clearCache() + }, [token]) + + function updateToken( + value: (string | undefined) + ): void { + if (value === undefined) { + reset() + } else { + setToken(value) + } + } + return [token, updateToken]; } export function useBackendInstanceToken( @@ -73,14 +88,12 @@ export function useBackendInstanceToken( function updateToken( value: (string | undefined) ): void { - console.log("seeting token", value) if (value === undefined) { reset() } else { setToken(value) } } - console.log("token", token) return [token, updateToken]; } diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts index f78de85dd..d15b3f6d7 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -113,7 +113,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "not-secret", + // token: "not-secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); @@ -154,7 +154,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "secret", + // token: "secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); @@ -190,7 +190,7 @@ describe("instance api interaction with details", () => { name: "instance_name", auth: { method: "token", - token: "not-secret", + // token: "not-secret", }, } as MerchantBackend.Instances.QueryInstancesResponse, }); diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index eae65d64c..32ed30c6f 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -198,6 +198,7 @@ export function useInstanceDetails(): HttpResponse< revalidateOnFocus: false, revalidateOnReconnect: false, refreshWhenOffline: false, + revalidateIfStale: false, errorRetryCount: 0, errorRetryInterval: 1, shouldRetryOnError: false, @@ -211,7 +212,7 @@ export function useInstanceDetails(): HttpResponse< type KYCStatus = | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects }; + | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; export function useInstanceKYCDetails(): HttpResponse< KYCStatus, @@ -220,7 +221,7 @@ export function useInstanceKYCDetails(): HttpResponse< const { fetcher } = useBackendInstanceRequest(); const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>, + HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, RequestError<MerchantBackend.ErrorDetail> >([`/private/kyc`], fetcher, { refreshInterval: 60 * 1000, diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts new file mode 100644 index 000000000..3544b4881 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -0,0 +1,223 @@ +/* + 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, + 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"; +const useSWR = _useSWR as unknown as SWRHook; + +const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { + "1": { + otp_description: "first device", + otp_algorithm: 1, + otp_device_id: "1", + otp_key: "123", + }, + "2": { + otp_description: "second device", + otp_algorithm: 0, + otp_device_id: "2", + otp_key: "456", + } +} + +export function useOtpDeviceAPI(): OtpDeviceAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendInstanceRequest(); + + 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, + }); + 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 deleteOtpDevice = async ( + deviceId: string, + ): Promise<HttpResponseOk<void>> => { + // delete MOCKED_DEVICES[deviceId] + // return Promise.resolve({ ok: true, data: undefined }); + const res = await request<void>(`/private/otp-devices/${deviceId}`, { + method: "DELETE", + }); + await mutateAll(/.*private\/otp-devices.*/); + return res; + }; + + return { + createOtpDevice, + updateOtpDevice, + deleteOtpDevice, + }; +} + +export interface OtpDeviceAPI { + createOtpDevice: ( + data: MerchantBackend.OTP.OtpDeviceAddDetails, + ) => Promise<HttpResponseOk<void>>; + updateOtpDevice: ( + id: string, + data: MerchantBackend.OTP.OtpDevicePatchDetails, + ) => Promise<HttpResponseOk<void>>; + deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; +} + +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 = false; + + 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 }; + } + 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, 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 }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts index d2831ecff..b3eecd754 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts @@ -25,16 +25,16 @@ import { useInstanceReserves, useReserveDetails, useReservesAPI, - useTipDetails, + useRewardDetails, } from "./reserves.js"; import { ApiMockEnvironment } from "./testing.js"; import { - API_AUTHORIZE_TIP, - API_AUTHORIZE_TIP_FOR_RESERVE, + API_AUTHORIZE_REWARD, + API_AUTHORIZE_REWARD_FOR_RESERVE, API_CREATE_RESERVE, API_DELETE_RESERVE, API_GET_RESERVE_BY_ID, - API_GET_TIP_BY_ID, + API_GET_REWARD_BY_ID, API_LIST_RESERVES, } from "./urls.js"; import * as tests from "@gnu-taler/web-util/testing"; @@ -48,7 +48,7 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -89,10 +89,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -115,10 +115,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }); }, @@ -138,13 +138,13 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "11", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "33", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -182,10 +182,10 @@ describe("reserve api interaction with listing", () => { reserves: [ { reserve_pub: "22", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, { reserve_pub: "33", - } as MerchantBackend.Tips.ReserveStatusEntry, + } as MerchantBackend.Rewards.ReserveStatusEntry, ], }, }); @@ -213,16 +213,16 @@ describe("reserve api interaction with listing", () => { }); describe("reserve api interaction with details", () => { - it("should evict cache when adding a tip for a specific reserve", async () => { + 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" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Tips.ReserveDetail, + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); @@ -246,37 +246,37 @@ describe("reserve api interaction with details", () => { if (!query.ok) return; expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], }); - env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { + env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { request: { amount: "USD:12", justification: "not", next_url: "http://taler.net", }, response: { - tip_id: "id2", - taler_tip_uri: "uri", - tip_expiration: { t_s: 1 }, - tip_status_url: "url", + 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" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], - } as MerchantBackend.Tips.ReserveDetail, + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); - api.authorizeTipReserve("11", { + api.authorizeRewardReserve("11", { amount: "USD:12", justification: "not", next_url: "http://taler.net", @@ -294,9 +294,9 @@ describe("reserve api interaction with details", () => { expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], }); }, @@ -308,16 +308,16 @@ describe("reserve api interaction with details", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should evict cache when adding a tip for a random reserve", async () => { + 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" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Tips.ReserveDetail, + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); @@ -341,37 +341,37 @@ describe("reserve api interaction with details", () => { if (!query.ok) return; expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], + rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], }); - env.addRequestExpectation(API_AUTHORIZE_TIP, { + env.addRequestExpectation(API_AUTHORIZE_REWARD, { request: { amount: "USD:12", justification: "not", next_url: "http://taler.net", }, response: { - tip_id: "id2", - taler_tip_uri: "uri", - tip_expiration: { t_s: 1 }, - tip_status_url: "url", + 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" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], - } as MerchantBackend.Tips.ReserveDetail, + } as MerchantBackend.Rewards.ReserveDetail, qparam: { - tips: "yes", + rewards: "yes", }, }); - api.authorizeTip({ + api.authorizeReward({ amount: "USD:12", justification: "not", next_url: "http://taler.net", @@ -387,9 +387,9 @@ describe("reserve api interaction with details", () => { expect(query.data).deep.equals({ accounts: [{ payto_uri: "payto://here" }], - tips: [ - { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, - { reason: "not", tip_id: "id2", total_amount: "USD:12" }, + rewards: [ + { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, + { reason: "not", reward_id: "id2", total_amount: "USD:12" }, ], }); }, @@ -402,15 +402,15 @@ describe("reserve api interaction with details", () => { }); }); -describe("reserve api interaction with tip details", () => { - it("should list tips", async () => { +describe("reserve api interaction with reward details", () => { + it("should list rewards", async () => { const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_GET_TIP_BY_ID("11"), { + env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { response: { total_picked_up: "USD:12", reason: "not", - } as MerchantBackend.Tips.TipDetails, + } as MerchantBackend.Rewards.RewardDetails, qparam: { pickups: "yes", }, @@ -418,7 +418,7 @@ describe("reserve api interaction with tip details", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useTipDetails("11"); + const query = useRewardDetails("11"); return { query }; }, {}, diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts index bb55b2474..b719bfbe6 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts @@ -31,11 +31,11 @@ export function useReservesAPI(): ReserveMutateAPI { const { request } = useBackendInstanceRequest(); const createReserve = async ( - data: MerchantBackend.Tips.ReserveCreateRequest, + data: MerchantBackend.Rewards.ReserveCreateRequest, ): Promise< - HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation> + HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> > => { - const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( + const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( `/private/reserves`, { method: "POST", @@ -49,12 +49,12 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - const authorizeTipReserve = async ( + const authorizeRewardReserve = async ( pub: string, - data: MerchantBackend.Tips.TipCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `/private/reserves/${pub}/authorize-tip`, + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/reserves/${pub}/authorize-reward`, { method: "POST", data, @@ -67,11 +67,11 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - const authorizeTip = async ( - data: MerchantBackend.Tips.TipCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( - `/private/tips`, + const authorizeReward = async ( + data: MerchantBackend.Rewards.RewardCreateRequest, + ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { + const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( + `/private/rewards`, { method: "POST", data, @@ -97,33 +97,33 @@ export function useReservesAPI(): ReserveMutateAPI { return res; }; - return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }; + return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; } export interface ReserveMutateAPI { createReserve: ( - data: MerchantBackend.Tips.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>; - authorizeTipReserve: ( + data: MerchantBackend.Rewards.ReserveCreateRequest, + ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; + authorizeRewardReserve: ( id: string, - data: MerchantBackend.Tips.TipCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; - authorizeTip: ( - data: MerchantBackend.Tips.TipCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; + 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.Tips.TippingReserveStatus, + MerchantBackend.Rewards.RewardReserveStatus, MerchantBackend.ErrorDetail > { const { fetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, + HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, RequestError<MerchantBackend.ErrorDetail> >([`/private/reserves`], fetcher); @@ -136,13 +136,13 @@ export function useInstanceReserves(): HttpResponse< export function useReserveDetails( reserveId: string, ): HttpResponse< - MerchantBackend.Tips.ReserveDetail, + MerchantBackend.Rewards.ReserveDetail, MerchantBackend.ErrorDetail > { const { reserveDetailFetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, + HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, RequestError<MerchantBackend.ErrorDetail> >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { refreshInterval: 0, @@ -158,15 +158,15 @@ export function useReserveDetails( return { loading: true }; } -export function useTipDetails( - tipId: string, -): HttpResponse<MerchantBackend.Tips.TipDetails, MerchantBackend.ErrorDetail> { - const { tipsDetailFetcher } = useBackendInstanceRequest(); +export function useRewardDetails( + rewardId: string, +): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { + const { rewardsDetailFetcher } = useBackendInstanceRequest(); const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Tips.TipDetails>, + HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, RequestError<MerchantBackend.ErrorDetail> - >([`/private/tips/${tipId}`], tipsDetailFetcher, { + >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts index 6b339c05a..00c5e95af 100644 --- a/packages/merchant-backoffice-ui/src/hooks/urls.ts +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -139,15 +139,15 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ //////////////////// export const API_CREATE_RESERVE: Query< - MerchantBackend.Tips.ReserveCreateRequest, - MerchantBackend.Tips.ReserveCreateConfirmation + MerchantBackend.Rewards.ReserveCreateRequest, + MerchantBackend.Rewards.ReserveCreateConfirmation > = { method: "POST", url: "http://backend/instances/default/private/reserves", }; export const API_LIST_RESERVES: Query< unknown, - MerchantBackend.Tips.TippingReserveStatus + MerchantBackend.Rewards.RewardReserveStatus > = { method: "GET", url: "http://backend/instances/default/private/reserves", @@ -155,34 +155,34 @@ export const API_LIST_RESERVES: Query< export const API_GET_RESERVE_BY_ID = ( pub: string, -): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ +): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ method: "GET", url: `http://backend/instances/default/private/reserves/${pub}`, }); -export const API_GET_TIP_BY_ID = ( +export const API_GET_REWARD_BY_ID = ( pub: string, -): Query<unknown, MerchantBackend.Tips.TipDetails> => ({ +): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ method: "GET", - url: `http://backend/instances/default/private/tips/${pub}`, + url: `http://backend/instances/default/private/rewards/${pub}`, }); -export const API_AUTHORIZE_TIP_FOR_RESERVE = ( +export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( pub: string, ): Query< - MerchantBackend.Tips.TipCreateRequest, - MerchantBackend.Tips.TipCreateConfirmation + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation > => ({ method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, + url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, }); -export const API_AUTHORIZE_TIP: Query< - MerchantBackend.Tips.TipCreateRequest, - MerchantBackend.Tips.TipCreateConfirmation +export const API_AUTHORIZE_REWARD: Query< + MerchantBackend.Rewards.RewardCreateRequest, + MerchantBackend.Rewards.RewardCreateConfirmation > = { method: "POST", - url: `http://backend/instances/default/private/tips`, + url: `http://backend/instances/default/private/rewards`, }; export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ @@ -211,7 +211,7 @@ export const API_GET_INSTANCE_BY_ID = ( export const API_GET_INSTANCE_KYC_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({ +): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ method: "GET", url: `http://backend/management/instances/${id}/kyc`, }); @@ -263,7 +263,7 @@ export const API_GET_CURRENT_INSTANCE: Query< export const API_GET_CURRENT_INSTANCE_KYC: Query< unknown, - MerchantBackend.Instances.AccountKycRedirects + MerchantBackend.KYC.AccountKycRedirects > = { method: "GET", url: `http://backend/instances/default/private/kyc`, diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts index 5c0932f27..7dee9f896 100644 --- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts +++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts @@ -19,6 +19,9 @@ import { Codec, buildCodecForObject, codecForBoolean, + codecForConstString, + codecForEither, + codecForString, } from "@gnu-taler/taler-util"; function parse_json_or_undefined<T>(str: string | undefined): T | undefined { @@ -31,29 +34,49 @@ function parse_json_or_undefined<T>(str: string | undefined): T | undefined { } export interface Settings { - advanceOrderMode: boolean + 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>, - <T extends keyof Settings>(key: T, value: Settings[T]) => void, + (s: Settings) => void, ] { - const { value, update } = useLocalStorage(SETTINGS_KEY); + const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); - const parsed: Settings = value ?? defaultSettings; - function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { - update({ ...parsed, [k]: v }); + // 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" } - return [parsed, updateField]; } + +export function datetimeFormatForSettings(s: Settings): string { + return dateFormatForSettings(s) + " HH:mm:ss" +}
\ No newline at end of file |