aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backend-ui/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backend-ui/src/hooks')
-rw-r--r--packages/merchant-backend-ui/src/hooks/async.ts76
-rw-r--r--packages/merchant-backend-ui/src/hooks/backend.ts262
-rw-r--r--packages/merchant-backend-ui/src/hooks/index.ts110
-rw-r--r--packages/merchant-backend-ui/src/hooks/instance.ts187
-rw-r--r--packages/merchant-backend-ui/src/hooks/listener.ts68
-rw-r--r--packages/merchant-backend-ui/src/hooks/notification.ts43
-rw-r--r--packages/merchant-backend-ui/src/hooks/notifications.ts48
-rw-r--r--packages/merchant-backend-ui/src/hooks/order.ts217
-rw-r--r--packages/merchant-backend-ui/src/hooks/product.ts223
-rw-r--r--packages/merchant-backend-ui/src/hooks/tips.ts159
-rw-r--r--packages/merchant-backend-ui/src/hooks/transfer.ts150
11 files changed, 1543 insertions, 0 deletions
diff --git a/packages/merchant-backend-ui/src/hooks/async.ts b/packages/merchant-backend-ui/src/hooks/async.ts
new file mode 100644
index 000000000..fd550043b
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/async.ts
@@ -0,0 +1,76 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useState } from "preact/hooks";
+import { cancelPendingRequest } from "./backend";
+
+export interface Options {
+ slowTolerance: number,
+}
+
+export interface AsyncOperationApi<T> {
+ request: (...a: any) => void,
+ cancel: () => void,
+ data: T | undefined,
+ isSlow: boolean,
+ isLoading: boolean,
+ error: string | undefined
+}
+
+export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
+ const [data, setData] = useState<T | undefined>(undefined);
+ const [isLoading, setLoading] = useState<boolean>(false);
+ const [error, setError] = useState<any>(undefined);
+ const [isSlow, setSlow] = useState(false)
+
+ const request = async (...args: any) => {
+ if (!fn) return;
+ setLoading(true);
+
+ const handler = setTimeout(() => {
+ setSlow(true)
+ }, tooLong)
+
+ try {
+ const result = await fn(...args);
+ setData(result);
+ } catch (error) {
+ setError(error);
+ }
+ setLoading(false);
+ setSlow(false)
+ clearTimeout(handler)
+ };
+
+ function cancel() {
+ cancelPendingRequest()
+ setLoading(false);
+ setSlow(false)
+ }
+
+ return {
+ request,
+ cancel,
+ data,
+ isSlow,
+ isLoading,
+ error
+ };
+}
diff --git a/packages/merchant-backend-ui/src/hooks/backend.ts b/packages/merchant-backend-ui/src/hooks/backend.ts
new file mode 100644
index 000000000..96b8f7139
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/backend.ts
@@ -0,0 +1,262 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { mutate, cache } from 'swr';
+import axios, { AxiosError, AxiosResponse } from 'axios'
+import { MerchantBackend } from '../declaration';
+import { useBackendContext } from '../context/backend';
+import { useEffect, useState } from 'preact/hooks';
+import { DEFAULT_REQUEST_TIMEOUT } from '../utils/constants';
+
+export function mutateAll(re: RegExp, value?: unknown): Array<Promise<any>> {
+ return cache.keys().filter(key => {
+ return re.test(key)
+ }).map(key => {
+ return mutate(key, value)
+ })
+}
+
+export type HttpResponse<T> = HttpResponseOk<T> | HttpResponseLoading<T> | HttpError;
+export type HttpResponsePaginated<T> = HttpResponseOkPaginated<T> | HttpResponseLoading<T> | HttpError;
+
+export interface RequestInfo {
+ url: string;
+ hasToken: boolean;
+ params: unknown;
+ data: unknown;
+}
+
+interface HttpResponseLoading<T> {
+ ok?: false;
+ loading: true;
+ clientError?: false;
+ serverError?: false;
+
+ data?: T;
+}
+export interface HttpResponseOk<T> {
+ ok: true;
+ loading?: false;
+ clientError?: false;
+ serverError?: false;
+
+ data: T;
+ info?: RequestInfo;
+}
+
+export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination
+
+export interface WithPagination {
+ loadMore: () => void;
+ loadMorePrev: () => void;
+ isReachingEnd?: boolean;
+ isReachingStart?: boolean;
+}
+
+export type HttpError = HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError;
+export interface SwrError {
+ info: unknown,
+ status: number,
+ message: string,
+}
+export interface HttpResponseServerError {
+ ok?: false;
+ loading?: false;
+ clientError?: false;
+ serverError: true;
+
+ error?: MerchantBackend.ErrorDetail;
+ status: number;
+ message: string;
+ info?: RequestInfo;
+}
+interface HttpResponseClientError {
+ ok?: false;
+ loading?: false;
+ clientError: true;
+ serverError?: false;
+
+ info?: RequestInfo;
+ isUnauthorized: boolean;
+ isNotfound: boolean;
+ status: number;
+ error?: MerchantBackend.ErrorDetail;
+ message: string;
+
+}
+
+interface HttpResponseUnexpectedError {
+ ok?: false;
+ loading?: false;
+ clientError?: false;
+ serverError?: false;
+
+ info?: RequestInfo;
+ status?: number;
+ error: unknown;
+ message: string;
+}
+
+type Methods = 'get' | 'post' | 'patch' | 'delete' | 'put';
+
+interface RequestOptions {
+ method?: Methods;
+ token?: string;
+ data?: unknown;
+ params?: unknown;
+}
+
+function buildRequestOk<T>(res: AxiosResponse<T>, url: string, hasToken: boolean): HttpResponseOk<T> {
+ return {
+ ok: true, data: res.data, info: {
+ params: res.config.params,
+ data: res.config.data,
+ url,
+ hasToken,
+ }
+ }
+}
+
+// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> {
+// if (isValidating) return {loading: true}
+// if (error) return buildRequestFailed()
+// }
+
+function buildRequestFailed(ex: AxiosError<MerchantBackend.ErrorDetail>, url: string, hasToken: boolean): HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError {
+ const status = ex.response?.status
+
+ const info: RequestInfo = {
+ data: ex.request?.data,
+ params: ex.request?.params,
+ url,
+ hasToken,
+ };
+
+ if (status && status >= 400 && status < 500) {
+ const error: HttpResponseClientError = {
+ clientError: true,
+ isNotfound: status === 404,
+ isUnauthorized: status === 401,
+ status,
+ info,
+ message: ex.response?.data?.hint || ex.message,
+ error: ex.response?.data
+ }
+ return error
+ }
+ if (status && status >= 500 && status < 600) {
+ const error: HttpResponseServerError = {
+ serverError: true,
+ status,
+ info,
+ message: `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` || ex.message,
+ error: ex.response?.data
+ }
+ return error;
+ }
+
+ const error: HttpResponseUnexpectedError = {
+ info,
+ status,
+ error: ex,
+ message: ex.message
+ }
+
+ return error
+}
+
+
+const CancelToken = axios.CancelToken;
+let source = CancelToken.source();
+
+export function cancelPendingRequest() {
+ source.cancel('canceled by the user')
+ source = CancelToken.source()
+}
+
+let removeAxiosCancelToken = false
+/**
+ * Jest mocking seems to break when using the cancelToken property.
+ * Using this workaround when testing while finding the correct solution
+ */
+export function setAxiosRequestAsTestingEnvironment() {
+ removeAxiosCancelToken = true
+}
+
+export async function request<T>(url: string, options: RequestOptions = {}): Promise<HttpResponseOk<T>> {
+ const headers = options.token ? { Authorization: `Bearer ${options.token}` } : undefined
+
+ try {
+ const res = await axios({
+ url,
+ responseType: 'json',
+ headers,
+ cancelToken: !removeAxiosCancelToken? source.token : undefined,
+ method: options.method || 'get',
+ data: options.data,
+ params: options.params,
+ timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
+ })
+ return buildRequestOk<T>(res, url, !!options.token)
+ } catch (e) {
+ const error = buildRequestFailed(e, url, !!options.token)
+ throw error
+ }
+
+}
+
+export function fetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
+ return request<T>(`${backend}${url}`, { token })
+}
+
+export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
+ const { url, token } = useBackendContext()
+
+ type Type = MerchantBackend.Instances.InstancesResponse;
+
+ const [result, setResult] = useState<HttpResponse<Type>>({ loading: true })
+
+ useEffect(() => {
+ request<Type>(`${url}/management/instances`, { token })
+ .then(data => setResult(data))
+ .catch(error => setResult(error))
+ }, [url, token])
+
+
+ return result
+}
+
+
+export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> {
+ const { url, token } = useBackendContext()
+
+ type Type = MerchantBackend.VersionResponse;
+
+ const [result, setResult] = useState<HttpResponse<Type>>({ loading: true })
+
+ useEffect(() => {
+ request<Type>(`${url}/config`, { token })
+ .then(data => setResult(data))
+ .catch(error => setResult(error))
+ }, [url, token])
+
+ return result
+}
diff --git a/packages/merchant-backend-ui/src/hooks/index.ts b/packages/merchant-backend-ui/src/hooks/index.ts
new file mode 100644
index 000000000..19d672ad3
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/index.ts
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { StateUpdater, useCallback, useState } from "preact/hooks";
+import { ValueOrFunction } from '../utils/types';
+
+
+const calculateRootPath = () => {
+ const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
+ return rootPath
+}
+
+export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
+ const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
+ const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
+
+ const checkedSetter = (v: ValueOrFunction<string>) => {
+ setTriedToLog('yes')
+ return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
+ }
+
+ const resetBackend = () => {
+ setTriedToLog(undefined)
+ }
+ return [value, !!triedToLog, checkedSetter, resetBackend]
+}
+
+export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
+ return useLocalStorage('backend-token')
+}
+
+export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
+ const [token, setToken] = useLocalStorage(`backend-token-${id}`)
+ const [defaultToken, defaultSetToken] = useBackendDefaultToken()
+
+ // instance named 'default' use the default token
+ if (id === 'default') {
+ return [defaultToken, defaultSetToken]
+ }
+
+ return [token, setToken]
+}
+
+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 useNotNullLocalStorage('lang-preference', defaultLang)
+}
+
+export function useLocalStorage(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];
+}
+
+export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
+ const [storedValue, setStoredValue] = useState<string>((): string => {
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
+ });
+
+ const setValue = (value: string | ((val: string) => string)) => {
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+ setStoredValue(valueToStore);
+ if (typeof window !== "undefined") {
+ if (!valueToStore) {
+ window.localStorage.removeItem(key)
+ } else {
+ window.localStorage.setItem(key, valueToStore);
+ }
+ }
+ };
+
+ return [storedValue, setValue];
+}
+
+
diff --git a/packages/merchant-backend-ui/src/hooks/instance.ts b/packages/merchant-backend-ui/src/hooks/instance.ts
new file mode 100644
index 000000000..14ab8de9c
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/instance.ts
@@ -0,0 +1,187 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { MerchantBackend } from '../declaration';
+import { useBackendContext } from '../context/backend';
+import { fetcher, HttpError, HttpResponse, HttpResponseOk, request, SwrError } from './backend';
+import useSWR, { mutate } from 'swr';
+import { useInstanceContext } from '../context/instance';
+
+
+interface InstanceAPI {
+ updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage) => Promise<void>;
+ deleteInstance: () => Promise<void>;
+ clearToken: () => Promise<void>;
+ setNewToken: (token: string) => Promise<void>;
+}
+
+export function useManagementAPI(instanceId: string) : InstanceAPI {
+ const { url, token } = useBackendContext()
+
+ const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => {
+ await request(`${url}/management/instances/${instanceId}`, {
+ method: 'patch',
+ token,
+ data: instance
+ })
+
+ mutate([`/private/`, token, url], null)
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`${url}/management/instances/${instanceId}`, {
+ method: 'delete',
+ token,
+ })
+
+ mutate([`/private/`, token, url], null)
+ }
+
+ const clearToken = async (): Promise<void> => {
+ await request(`${url}/management/instances/${instanceId}/auth`, {
+ method: 'post',
+ token,
+ data: { method: 'external' }
+ })
+
+ mutate([`/private/`, token, url], null)
+ }
+
+ const setNewToken = async (newToken: string): Promise<void> => {
+ await request(`${url}/management/instances/${instanceId}/auth`, {
+ method: 'post',
+ token,
+ data: { method: 'token', token: newToken }
+ })
+
+ mutate([`/private/`, token, url], null)
+ }
+
+ return { updateInstance, deleteInstance, setNewToken, clearToken }
+}
+
+export function useInstanceAPI(): InstanceAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ };
+
+ const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => {
+ await request(`${url}/private/`, {
+ method: 'patch',
+ token,
+ data: instance
+ })
+
+ if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null)
+ mutate([`/private/`, token, url], null)
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`${url}/private/`, {
+ method: 'delete',
+ token: adminToken,
+ })
+
+ if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null)
+ mutate([`/private/`, token, url], null)
+ }
+
+ const clearToken = async (): Promise<void> => {
+ await request(`${url}/private/auth`, {
+ method: 'post',
+ token,
+ data: { method: 'external' }
+ })
+
+ mutate([`/private/`, token, url], null)
+ }
+
+ const setNewToken = async (newToken: string): Promise<void> => {
+ await request(`${url}/private/auth`, {
+ method: 'post',
+ token,
+ data: { method: 'token', token: newToken }
+ })
+
+ mutate([`/private/`, token, url], null)
+ }
+
+ return { updateInstance, deleteInstance, setNewToken, clearToken }
+}
+
+
+export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, HttpError>([`/private/`, token, url], fetcher, {
+ 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
+ return {loading: true}
+}
+
+export function useManagedInstanceDetails(instanceId: string): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
+ const { url, token } = useBackendContext();
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, HttpError>([`/management/instances/${instanceId}`, token, url], fetcher, {
+ 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
+ return {loading: true}
+}
+
+export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
+ const { url } = useBackendContext()
+ const { token } = useInstanceContext();
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, HttpError>(['/management/instances', token, url], fetcher)
+
+ if (isValidating) return {loading:true, data: data?.data}
+ if (data) return data
+ if (error) return error
+ return {loading: true}
+}
diff --git a/packages/merchant-backend-ui/src/hooks/listener.ts b/packages/merchant-backend-ui/src/hooks/listener.ts
new file mode 100644
index 000000000..231ed6c87
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/listener.ts
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useState } from "preact/hooks";
+
+/**
+ * returns subscriber and activator
+ * subscriber will receive a method (listener) that will be call when the activator runs.
+ * the result of calling the listener will be sent to @action
+ *
+ * @param action from <T> to <R>
+ * @returns activator and subscriber, undefined activator means that there is not subscriber
+ */
+
+export function useListener<T, R = any>(action: (r: T) => Promise<R>): [undefined | (() => Promise<R>), (listener?: () => T) => void] {
+ type RunnerHandler = { toBeRan?: () => Promise<R>; };
+ const [state, setState] = useState<RunnerHandler>({});
+
+ /**
+ * subscriber will receive a method that will be call when the activator runs
+ *
+ * @param listener function to be run when the activator runs
+ */
+ const subscriber = (listener?: () => T) => {
+ if (listener) {
+ setState({
+ toBeRan: () => {
+ const whatWeGetFromTheListener = listener();
+ return action(whatWeGetFromTheListener);
+ }
+ });
+ } else {
+ setState({
+ toBeRan: undefined
+ })
+ }
+ };
+
+ /**
+ * activator will call runner if there is someone subscribed
+ */
+ const activator = state.toBeRan ? async () => {
+ if (state.toBeRan) {
+ return state.toBeRan();
+ }
+ return Promise.reject();
+ } : undefined;
+
+ return [activator, subscriber];
+}
diff --git a/packages/merchant-backend-ui/src/hooks/notification.ts b/packages/merchant-backend-ui/src/hooks/notification.ts
new file mode 100644
index 000000000..d1dfbff2c
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/notification.ts
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useCallback, useState } from "preact/hooks";
+import { Notification } from '../utils/types';
+
+interface Result {
+ notification?: Notification;
+ pushNotification: (n: Notification) => void;
+ removeNotification: () => void;
+}
+
+export function useNotification(): Result {
+ const [notification, setNotifications] = useState<Notification|undefined>(undefined)
+
+ const pushNotification = useCallback((n: Notification): void => {
+ setNotifications(n)
+ },[])
+
+ const removeNotification = useCallback(() => {
+ setNotifications(undefined)
+ },[])
+
+ return { notification, pushNotification, removeNotification }
+}
diff --git a/packages/merchant-backend-ui/src/hooks/notifications.ts b/packages/merchant-backend-ui/src/hooks/notifications.ts
new file mode 100644
index 000000000..1c0c37308
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/notifications.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useState } from "preact/hooks";
+import { Notification } from '../utils/types';
+
+interface Result {
+ notifications: Notification[];
+ pushNotification: (n: Notification) => void;
+ removeNotification: (n: Notification) => void;
+}
+
+type NotificationWithDate = Notification & { since: Date }
+
+export function useNotifications(initial: Notification[] = [], timeout = 3000): Result {
+ const [notifications, setNotifications] = useState<(NotificationWithDate)[]>(initial.map(i => ({...i, since: new Date() })))
+
+ const pushNotification = (n: Notification): void => {
+ const entry = { ...n, since: new Date() }
+ setNotifications(ns => [...ns, entry])
+ if (n.type !== 'ERROR') setTimeout(() => {
+ setNotifications(ns => ns.filter(x => x.since !== entry.since))
+ }, timeout)
+ }
+
+ const removeNotification = (notif: Notification) => {
+ setNotifications((ns: NotificationWithDate[]) => ns.filter(n => n !== notif))
+ }
+ return { notifications, pushNotification, removeNotification }
+}
diff --git a/packages/merchant-backend-ui/src/hooks/order.ts b/packages/merchant-backend-ui/src/hooks/order.ts
new file mode 100644
index 000000000..4a17eac30
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/order.ts
@@ -0,0 +1,217 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useEffect, useState } from 'preact/hooks';
+import useSWR from 'swr';
+import { useBackendContext } from '../context/backend';
+import { useInstanceContext } from '../context/instance';
+import { MerchantBackend } from '../declaration';
+import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants';
+import { fetcher, HttpError, HttpResponse, HttpResponseOk, HttpResponsePaginated, mutateAll, request } from './backend';
+
+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 orderFetcher<T>(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?: number): Promise<HttpResponseOk<T>> {
+ const date_ms = delta && delta < 0 && searchDate ? searchDate.getTime() + 1 : searchDate?.getTime()
+ 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_ms !== undefined) params.date_ms = date_ms
+ return request<T>(`${backend}${url}`, { token, params })
+}
+
+
+export function useOrderAPI(): OrderAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext()
+ const { token: instanceToken, id, admin } = useInstanceContext()
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const createOrder = async (data: MerchantBackend.Orders.PostOrderRequest): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
+ const res = await request<MerchantBackend.Orders.PostOrderResponse>(`${url}/private/orders`, {
+ method: 'post',
+ token,
+ data
+ })
+ await mutateAll(/@"\/private\/orders"@/)
+ return res
+ }
+ const refundOrder = async (orderId: string, data: MerchantBackend.Orders.RefundRequest): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {
+ mutateAll(/@"\/private\/orders"@/)
+ return request<MerchantBackend.Orders.MerchantRefundResponse>(`${url}/private/orders/${orderId}/refund`, {
+ method: 'post',
+ token,
+ data
+ })
+
+ // return res
+ }
+
+ const forgetOrder = async (orderId: string, data: MerchantBackend.Orders.ForgetRequest): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/)
+ return request(`${url}/private/orders/${orderId}/forget`, {
+ method: 'patch',
+ token,
+ data
+ })
+
+ }
+ const deleteOrder = async (orderId: string): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/)
+ return request(`${url}/private/orders/${orderId}`, {
+ method: 'delete',
+ token
+ })
+ }
+
+ const getPaymentURL = async (orderId: string): Promise<HttpResponseOk<string>> => {
+ return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(`${url}/private/orders/${orderId}`, {
+ method: 'get',
+ token
+ }).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 useOrderDetails(oderId: string): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ };
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, HttpError>([`/private/orders/${oderId}`, token, url], 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
+ return { loading: true }
+}
+
+export interface InstanceOrderFilter {
+ paid?: YesOrNo;
+ refunded?: YesOrNo;
+ wired?: YesOrNo;
+ date?: Date;
+}
+
+export function useInstanceOrders(args?: InstanceOrderFilter, updateFilter?: (d: Date) => void): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ 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>, HttpError>(
+ [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, totalBefore],
+ orderFetcher,
+ )
+ const { data: afterData, error: afterError, isValidating: loadingAfter } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
+ [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, -totalAfter],
+ orderFetcher,
+ )
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<HttpResponse<MerchantBackend.Orders.OrderHistory>>({ loading: true })
+ const [lastAfter, setLastAfter] = useState<HttpResponse<MerchantBackend.Orders.OrderHistory>>({ loading: true })
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData)
+ if (beforeData) setLastBefore(beforeData)
+ }, [afterData, beforeData])
+
+ // this has problems when there are some ids missing
+
+ if (beforeError) return beforeError
+ if (afterError) return afterError
+
+
+ const pagination = {
+ isReachingEnd: afterData && afterData.data.orders.length < totalAfter,
+ isReachingStart: (!args?.date) || (beforeData && beforeData.data.orders.length < totalBefore),
+ loadMore: () => {
+ if (!afterData) 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 && updateFilter) updateFilter(new Date(from))
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData) 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 && updateFilter) updateFilter(new Date(from))
+ }
+ },
+ }
+
+ 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 }
+
+}
+
diff --git a/packages/merchant-backend-ui/src/hooks/product.ts b/packages/merchant-backend-ui/src/hooks/product.ts
new file mode 100644
index 000000000..4fc8bccb7
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/product.ts
@@ -0,0 +1,223 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { useEffect } from "preact/hooks";
+import useSWR, { trigger, useSWRInfinite, cache, mutate } from "swr";
+import { useBackendContext } from "../context/backend";
+// import { useFetchContext } from '../context/fetch';
+import { useInstanceContext } from "../context/instance";
+import { MerchantBackend, WithId } from "../declaration";
+import {
+ fetcher,
+ HttpError,
+ HttpResponse,
+ HttpResponseOk,
+ mutateAll,
+ request,
+} from "./backend";
+
+export interface ProductAPI {
+ 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>;
+}
+
+export function useProductAPI(): ProductAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin
+ ? {
+ url: baseUrl,
+ token: adminToken,
+ }
+ : {
+ url: `${baseUrl}/instances/${id}`,
+ token: instanceToken,
+ };
+
+ const createProduct = async (
+ data: MerchantBackend.Products.ProductAddDetail
+ ): Promise<void> => {
+ await request(`${url}/private/products`, {
+ method: "post",
+ token,
+ data,
+ });
+
+ await mutateAll(/@"\/private\/products"@/, null);
+ };
+
+ const updateProduct = async (
+ productId: string,
+ data: MerchantBackend.Products.ProductPatchDetail
+ ): Promise<void> => {
+ const r = await request(`${url}/private/products/${productId}`, {
+ method: "patch",
+ token,
+ data,
+ });
+
+ await mutateAll(/@"\/private\/products\/.*"@/);
+ return Promise.resolve();
+ };
+
+ const deleteProduct = async (productId: string): Promise<void> => {
+ await request(`${url}/private/products/${productId}`, {
+ method: "delete",
+ token,
+ });
+
+ await mutateAll(/@"\/private\/products"@/);
+ };
+
+ const lockProduct = async (
+ productId: string,
+ data: MerchantBackend.Products.LockRequest
+ ): Promise<void> => {
+ await request(`${url}/private/products/${productId}/lock`, {
+ method: "post",
+ token,
+ data,
+ });
+
+ await mutateAll(/@"\/private\/products"@/);
+ };
+
+ return { createProduct, updateProduct, deleteProduct, lockProduct };
+}
+
+export function useInstanceProducts(): HttpResponse<
+ (MerchantBackend.Products.ProductDetail & WithId)[]
+> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+ // const { useSWR, useSWRInfinite } = useFetchContext();
+
+ const { url, token } = !admin
+ ? {
+ url: baseUrl,
+ token: baseToken,
+ }
+ : {
+ url: `${baseUrl}/instances/${id}`,
+ token: instanceToken,
+ };
+
+ const {
+ data: list,
+ error: listError,
+ isValidating: listLoading,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
+ HttpError
+ >([`/private/products`, token, url], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ const {
+ data: products,
+ error: productError,
+ setSize,
+ size,
+ } = useSWRInfinite<
+ HttpResponseOk<MerchantBackend.Products.ProductDetail>,
+ HttpError
+ >(
+ (pageIndex: number) => {
+ if (!list?.data || !list.data.products.length || listError || listLoading)
+ return null;
+ return [
+ `/private/products/${list.data.products[pageIndex].product_id}`,
+ token,
+ url,
+ ];
+ },
+ fetcher,
+ {
+ revalidateAll: true,
+ }
+ );
+
+ useEffect(() => {
+ if (list?.data && list.data.products.length > 0) {
+ setSize(list.data.products.length);
+ }
+ }, [list?.data.products.length, listLoading]);
+
+ if (listLoading) return { loading: true, data: [] };
+ if (listError) return listError;
+ if (productError) return productError;
+ if (list?.data && list.data.products.length === 0) {
+ return { ok: true, data: [] };
+ }
+ 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
+): HttpResponse<MerchantBackend.Products.ProductDetail> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin
+ ? {
+ url: baseUrl,
+ token: baseToken,
+ }
+ : {
+ url: `${baseUrl}/instances/${id}`,
+ token: instanceToken,
+ };
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Products.ProductDetail>,
+ HttpError
+ >([`/private/products/${productId}`, token, url], 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;
+ return { loading: true };
+}
diff --git a/packages/merchant-backend-ui/src/hooks/tips.ts b/packages/merchant-backend-ui/src/hooks/tips.ts
new file mode 100644
index 000000000..345e1faa5
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/tips.ts
@@ -0,0 +1,159 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import useSWR from 'swr';
+import { useBackendContext } from '../context/backend';
+import { useInstanceContext } from '../context/instance';
+import { MerchantBackend } from '../declaration';
+import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request } from './backend';
+
+
+export function useReservesAPI(): ReserveMutateAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ };
+
+ const createReserve = async (data: MerchantBackend.Tips.ReserveCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`, {
+ method: 'post',
+ token,
+ data
+ });
+
+ await mutateAll(/@"\/private\/reserves"@/);
+
+ return res
+ };
+
+ const authorizeTipReserve = async (pub: string, data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`, {
+ method: 'post',
+ token,
+ data
+ });
+ await mutateAll(/@"\/private\/reserves"@/);
+
+ return res
+ };
+
+ const authorizeTip = async (data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, {
+ method: 'post',
+ token,
+ data
+ });
+
+ await mutateAll(/@"\/private\/reserves"@/);
+
+ return res
+ };
+
+ const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
+ const res = await request<void>(`${url}/private/reserves/${pub}`, {
+ method: 'delete',
+ token,
+ });
+
+ await mutateAll(/@"\/private\/reserves"@/);
+
+ return res
+ };
+
+
+ return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
+}
+
+export interface ReserveMutateAPI {
+ createReserve: (data: MerchantBackend.Tips.ReserveCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
+ authorizeTipReserve: (id: string, data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
+ authorizeTip: (data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
+ deleteReserve: (id: string) => Promise<HttpResponse<void>>;
+}
+
+export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, HttpError>([`/private/reserves`, token, url], fetcher)
+
+ if (isValidating) return { loading: true, data: data?.data }
+ if (data) return data
+ if (error) return error
+ return { loading: true }
+}
+
+
+export function useReserveDetails(reserveId: string): HttpResponse<MerchantBackend.Tips.ReserveDetail> {
+ const { url: baseUrl } = useBackendContext();
+ const { token, id: instanceId, admin } = useInstanceContext();
+
+ const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, HttpError>([`/private/reserves/${reserveId}`, token, url], 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
+ return { loading: true }
+}
+
+export function useTipDetails(tipId: string): HttpResponse<MerchantBackend.Tips.TipDetails> {
+ const { url: baseUrl } = useBackendContext();
+ const { token, id: instanceId, admin } = useInstanceContext();
+
+ const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
+
+ const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TipDetails>, HttpError>([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, {
+ 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
+ return { loading: true }
+}
+
+export function reserveDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
+ return request<T>(`${backend}${url}`, { token, params: {
+ tips: 'yes'
+ } })
+}
+
+export function tipsDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> {
+ return request<T>(`${backend}${url}`, { token, params: {
+ pickups: 'yes'
+ } })
+}
diff --git a/packages/merchant-backend-ui/src/hooks/transfer.ts b/packages/merchant-backend-ui/src/hooks/transfer.ts
new file mode 100644
index 000000000..482f00dc5
--- /dev/null
+++ b/packages/merchant-backend-ui/src/hooks/transfer.ts
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { MerchantBackend } from '../declaration';
+import { useBackendContext } from '../context/backend';
+import { request, mutateAll, HttpResponse, HttpError, HttpResponseOk, HttpResponsePaginated } from './backend';
+import useSWR from 'swr';
+import { useInstanceContext } from '../context/instance';
+import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants';
+import { useEffect, useState } from 'preact/hooks';
+
+async function transferFetcher<T>(url: string, token: string, backend: string, payto_uri?: string, verified?: string, position?: string, delta?: number): Promise<HttpResponseOk<T>> {
+ const params: any = {}
+ if (payto_uri !== undefined) params.payto_uri = payto_uri
+ if (verified !== undefined) params.verified = verified
+ if (delta !== undefined) {
+ // if (delta > 0) {
+ // params.after = searchDate?.getTime()
+ // } else {
+ // params.before = searchDate?.getTime()
+ // }
+ params.limit = delta
+ }
+ if (position !== undefined) params.offset = position
+
+ return request<T>(`${backend}${url}`, { token, params })
+}
+
+export function useTransferAPI(): TransferAPI {
+ const { url: baseUrl, token: adminToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: adminToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ };
+
+ const informTransfer = async (data: MerchantBackend.Transfers.TransferInformation): Promise<HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>> => {
+ mutateAll(/@"\/private\/transfers"@/);
+
+ return request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(`${url}/private/transfers`, {
+ method: 'post',
+ token,
+ data
+ });
+ };
+
+ return { informTransfer };
+}
+
+export interface TransferAPI {
+ informTransfer: (data: MerchantBackend.Transfers.TransferInformation) => Promise<HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse>>;
+}
+
+export interface InstanceTransferFilter {
+ payto_uri?: string;
+ verified?: 'yes' | 'no';
+ position?: string;
+}
+
+
+export function useInstanceTransfers(args?: InstanceTransferFilter, updatePosition?: (id: string) => void): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> {
+ const { url: baseUrl, token: baseToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const { url, token } = !admin ? {
+ url: baseUrl, token: baseToken
+ } : {
+ url: `${baseUrl}/instances/${id}`, token: instanceToken
+ }
+
+ 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>, HttpError>(
+ [`/private/transfers`, token, url, args?.payto_uri, args?.verified, args?.position, totalBefore],
+ transferFetcher,
+ )
+ const { data: afterData, error: afterError, isValidating: loadingAfter } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(
+ [`/private/transfers`, token, url, args?.payto_uri, args?.verified, args?.position, -totalAfter],
+ transferFetcher,
+ )
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<HttpResponse<MerchantBackend.Transfers.TransferList>>({ loading: true })
+ const [lastAfter, setLastAfter] = useState<HttpResponse<MerchantBackend.Transfers.TransferList>>({ loading: true })
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData)
+ if (beforeData) setLastBefore(beforeData)
+ }, [afterData, beforeData])
+
+ // this has problems when there are some ids missing
+
+ if (beforeError) return beforeError
+ if (afterError) return afterError
+
+ const pagination = {
+ isReachingEnd: afterData && afterData.data.transfers.length < totalAfter,
+ isReachingStart: (!args?.position) || (beforeData && beforeData.data.transfers.length < totalBefore),
+ loadMore: () => {
+ if (!afterData) 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) 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)
+ }
+ },
+ }
+
+ 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 }
+}
+
+