summaryrefslogtreecommitdiff
path: root/packages/web-util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util')
-rw-r--r--packages/web-util/src/context/activity.ts65
-rw-r--r--packages/web-util/src/context/bank-api.ts202
-rw-r--r--packages/web-util/src/context/index.ts5
-rw-r--r--packages/web-util/src/context/merchant-api.ts226
-rw-r--r--packages/web-util/src/context/navigation.ts102
-rw-r--r--packages/web-util/src/context/wallet-integration.ts83
-rw-r--r--packages/web-util/src/index.browser.ts1
-rw-r--r--packages/web-util/src/utils/route.ts129
8 files changed, 812 insertions, 1 deletions
diff --git a/packages/web-util/src/context/activity.ts b/packages/web-util/src/context/activity.ts
new file mode 100644
index 000000000..570729471
--- /dev/null
+++ b/packages/web-util/src/context/activity.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ObservabilityEvent, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util";
+
+type Listener<Event> = (e: Event) => void;
+type Unsuscriber = () => void;
+export type Suscriber<Event> = (fn: Listener<Event>) => Unsuscriber;
+
+export class ActiviyTracker<Event> {
+ private observers = new Array<Listener<Event>>();
+ notify(data: Event) {
+ this.observers.forEach((observer) => observer(data))
+ }
+ subscribe(func: Listener<Event>): Unsuscriber {
+ this.observers.push(func);
+ return () => {
+ this.observers.forEach((observer, index) => {
+ if (observer === func) {
+ this.observers.splice(index, 1);
+ }
+ });
+ };
+ }
+}
+
+/**
+ * build http client with cache breaker due to SWR
+ * @param url
+ * @returns
+ */
+export interface APIClient<T, C> {
+ getRemoteConfig(): Promise<C>;
+ VERSION: string;
+ lib: T,
+ onActivity: Suscriber<ObservabilityEvent>;
+ cancelRequest(id: string): void;
+}
+
+export interface MerchantLib {
+ management: TalerMerchantManagementHttpClient;
+ authenticate: TalerAuthenticationHttpClient;
+ instance: (instanceId: string) => TalerMerchantInstanceHttpClient;
+ impersonate: (instanceId: string) => TalerAuthenticationHttpClient;
+}
+
+export interface BankLib {
+ bank: TalerCoreBankHttpClient;
+ conversion: TalerBankConversionHttpClient;
+ auth: (user: string) => TalerAuthenticationHttpClient;
+}
+
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
new file mode 100644
index 000000000..645eda183
--- /dev/null
+++ b/packages/web-util/src/context/bank-api.ts
@@ -0,0 +1,202 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerAuthenticationHttpClient,
+ TalerBankConversionCacheEviction,
+ TalerBankConversionHttpClient,
+ TalerCoreBankCacheEviction,
+ TalerCoreBankHttpClient,
+ TalerCorebankApi,
+ TalerError
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { APIClient, ActiviyTracker, BankLib, Suscriber } from "./activity.js";
+import { useTranslationContext } from "./translation.js";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type BankContextType = {
+ url: URL;
+ config: TalerCorebankApi.Config;
+ lib: BankLib;
+ hints: VersionHint[];
+ onActivity: Suscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const BankContext = createContext<BankContextType>(undefined);
+
+export const useBankCoreApiContext = (): BankContextType => useContext(BankContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ conversion?: CacheEvictor<TalerBankConversionCacheEviction>;
+ bank?: CacheEvictor<TalerCoreBankCacheEviction>;
+}
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+export const BankApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+ evictors = {},
+}: {
+ baseUrl: URL;
+ children: ComponentChildren;
+ evictors?: Evictors,
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] = useState<ConfigResult<TalerCorebankApi.Config>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = buildBankApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ getRemoteConfig()
+ .then((config) => {
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof TalerError) {
+ setChecked({ type: "error", error });
+ }
+ });
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, { children: h("div", {}, "checking compatibility with server...") });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: BankContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(BankContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildBankApiClient(url: URL, evictors: Evictors,
+): APIClient<BankLib, TalerCorebankApi.Config> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const bank = new TalerCoreBankHttpClient(
+ url.href,
+ httpLib,
+ evictors.bank,
+ );
+ const conversion = new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPI().href,
+ httpLib,
+ evictors.conversion,
+ );
+ const auth = (user: string) =>
+ new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI(user).href,
+ user,
+ httpLib,
+ );
+
+ async function getRemoteConfig() {
+ const resp = await bank.getConfig()
+ return resp.body
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: bank.PROTOCOL_VERSION,
+ lib: {
+ bank, conversion, auth
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+
+export const BankApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: BankContextType
+ children: ComponentChildren;
+}): VNode => {
+ return h(BankContext.Provider, {
+ value,
+ children,
+ });
+}
diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts
index 9ed3ef645..0e28b844a 100644
--- a/packages/web-util/src/context/index.ts
+++ b/packages/web-util/src/context/index.ts
@@ -4,4 +4,7 @@ export {
TranslationProvider,
useTranslationContext
} from "./translation.js";
-
+export * from "./bank-api.js";
+export * from "./merchant-api.js";
+export * from "./navigation.js";
+export * from "./wallet-integration.js";
diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts
new file mode 100644
index 000000000..79c79ee9c
--- /dev/null
+++ b/packages/web-util/src/context/merchant-api.ts
@@ -0,0 +1,226 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerAuthenticationHttpClient,
+ TalerError,
+ TalerMerchantApi,
+ TalerMerchantInstanceCacheEviction,
+ TalerMerchantInstanceHttpClient,
+ TalerMerchantManagementCacheEviction,
+ TalerMerchantManagementHttpClient,
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import {
+ APIClient,
+ ActiviyTracker,
+ MerchantLib,
+ Suscriber,
+} from "./activity.js";
+import { useTranslationContext } from "./translation.js";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type MerchantContextType = {
+ url: URL;
+ config: TalerMerchantApi.VersionResponse;
+ lib: MerchantLib;
+ hints: VersionHint[];
+ onActivity: Suscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// FIXME: below
+// @ts-expect-error default value to undefined, should it be another thing?
+const MerchantContext = createContext<MerchantContextType>(undefined);
+
+export const useMerchantApiContext = (): MerchantContextType =>
+ useContext(MerchantContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ management?: CacheEvictor<TalerMerchantManagementCacheEviction>;
+ instance?: (
+ instanceId: string,
+ ) => CacheEvictor<TalerMerchantInstanceCacheEviction>;
+};
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+export const MerchantApiProvider = ({
+ baseUrl,
+ children,
+ evictors = {},
+ frameOnError,
+}: {
+ baseUrl: URL;
+ evictors?: Evictors;
+ children: ComponentChildren;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<TalerMerchantApi.VersionResponse>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildMerchantApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ getRemoteConfig()
+ .then((config) => {
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof TalerError) {
+ setChecked({ type: "error", error });
+ }
+ });
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: MerchantContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(MerchantContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildMerchantApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<MerchantLib, TalerMerchantApi.VersionResponse> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const management = new TalerMerchantManagementHttpClient(
+ url.href,
+ httpLib,
+ evictors.management,
+ );
+ const instance = (instanceId: string) =>
+ new TalerMerchantInstanceHttpClient(
+ management.getSubInstanceAPI(instanceId).href,
+ httpLib,
+ evictors.instance ? evictors.instance(instanceId) : undefined,
+ );
+ const authenticate = new TalerAuthenticationHttpClient(
+ management.getAuthenticationAPI().href,
+ "default",
+ httpLib,
+ );
+ const impersonate = (instanceId: string) =>
+ new TalerAuthenticationHttpClient(
+ instance(instanceId).getAuthenticationAPI().href,
+ instanceId,
+ httpLib,
+ );
+
+ async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> {
+ const resp = await management.getConfig();
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: management.PROTOCOL_VERSION,
+ lib: {
+ management,
+ authenticate,
+ impersonate,
+ instance,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const MerchantApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: MerchantContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(MerchantContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts
new file mode 100644
index 000000000..a2fe3ff12
--- /dev/null
+++ b/packages/web-util/src/context/navigation.ts
@@ -0,0 +1,102 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { AppLocation, ObjectOf, Location, findMatch, RouteDefinition } from "../utils/route.js";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
+ pagesMap: T,
+): Location<T> | undefined {
+ const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
+ const { path, params } = useNavigationContext();
+
+ return findMatch(pagesMap, pageList, path, params);
+}
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = {
+ path: string;
+ params: Record<string, string>;
+ navigateTo: (path: AppLocation) => void;
+ // addNavigationListener: (listener: (path: string, params: Record<string, string>) => void) => (() => void);
+};
+
+// @ts-expect-error should not be used without provider
+const Context = createContext<Type>(undefined);
+
+export const useNavigationContext = (): Type => useContext(Context);
+
+function getPathAndParamsFromWindow() {
+ const path =
+ typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
+ const params: Record<string, string> = {};
+ if (typeof window !== "undefined") {
+ for (const [key, value] of new URLSearchParams(window.location.search)) {
+ params[key] = value;
+ }
+ }
+ return { path, params };
+}
+
+const { path: initialPath, params: initialParams } =
+ getPathAndParamsFromWindow();
+
+// there is a possibility that if the browser does a redirection
+// (which doesn't go through navigatTo function) and that executed
+// too early (before addEventListener runs) it won't be taking
+// into account
+const PopStateEventType = "popstate";
+
+export const BrowserHashNavigationProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const [{ path, params }, setState] = useState({
+ path: initialPath,
+ params: initialParams,
+ });
+ if (typeof window === "undefined") {
+ throw Error(
+ "Can't use BrowserHashNavigationProvider if there is no window object",
+ );
+ }
+ function navigateTo(path: string) {
+ const { params } = getPathAndParamsFromWindow();
+ setState({ path, params });
+ window.location.href = path;
+ }
+
+ useEffect(() => {
+ function eventListener() {
+ setState(getPathAndParamsFromWindow());
+ }
+ window.addEventListener(PopStateEventType, eventListener);
+ return () => {
+ window.removeEventListener(PopStateEventType, eventListener);
+ };
+ }, []);
+ return h(Context.Provider, {
+ value: { path, params, navigateTo },
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/wallet-integration.ts b/packages/web-util/src/context/wallet-integration.ts
new file mode 100644
index 000000000..e14988ed1
--- /dev/null
+++ b/packages/web-util/src/context/wallet-integration.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { stringifyTalerUri, TalerUri } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ * https://docs.taler.net/design-documents/039-taler-browser-integration.html
+ *
+ * @param uri
+ */
+function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) {
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", stringifyTalerUri(uri));
+
+ document.head.appendChild(meta);
+
+ let walletFound = false;
+ window.addEventListener("beforeunload", () => {
+ walletFound = true;
+ });
+ setTimeout(() => {
+ if (!walletFound && onNotFound) {
+ onNotFound();
+ }
+ }, 10); //very short timeout
+}
+interface Type {
+ /**
+ * Tell the active wallet that an action is found
+ *
+ * @param uri
+ * @returns
+ */
+ publishTalerAction: (uri: TalerUri, onNotFound?: () => void) => void;
+}
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const Context = createContext<Type>(undefined);
+
+export const useTalerWalletIntegrationAPI = (): Type => useContext(Context);
+
+export const TalerWalletIntegrationBrowserProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const value: Type = {
+ publishTalerAction: createHeadMetaTag,
+ };
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export const TalerWalletIntegrationTestingProvider = ({
+ children,
+ value,
+}: {
+ children: ComponentChildren;
+ value: Type;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 82c399bfd..2f3b57b8d 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -3,6 +3,7 @@ export * from "./utils/request.js";
export * from "./utils/http-impl.browser.js";
export * from "./utils/http-impl.sw.js";
export * from "./utils/observable.js";
+export * from "./utils/route.js";
export * from "./context/index.js";
export * from "./components/index.js";
export * from "./forms/index.js";
diff --git a/packages/web-util/src/utils/route.ts b/packages/web-util/src/utils/route.ts
new file mode 100644
index 000000000..4f8a020f6
--- /dev/null
+++ b/packages/web-util/src/utils/route.ts
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+declare const __location: unique symbol;
+/**
+ * special string that defined a location in the application
+ *
+ * this help to prevent wrong path
+ */
+export type AppLocation = string & {
+ [__location]: true;
+};
+
+export type EmptyObject = Record<string, never>;
+
+export function urlPattern<
+ T extends Record<string, string | undefined> = EmptyObject,
+>(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
+ const url = reverse as (p: T) => AppLocation;
+ return {
+ pattern: new RegExp(pattern),
+ url,
+ };
+}
+
+/**
+ * defines a location in the app
+ *
+ * pattern: how a string will trigger this location
+ * url(): how a state serialize to a location
+ */
+
+export type ObjectOf<T> = Record<string, T> | EmptyObject;
+
+export type RouteDefinition<
+ T extends ObjectOf<string | undefined> = EmptyObject,
+> = {
+ pattern: RegExp;
+ url: (p: T) => AppLocation;
+};
+
+const nullRountDef = {
+ pattern: new RegExp(/.*/),
+ url: () => "" as AppLocation,
+};
+export function buildNullRoutDefinition<
+ T extends ObjectOf<string>,
+>(): RouteDefinition<T> {
+ return nullRountDef;
+}
+
+/**
+ * Search path in the pageList
+ * get the values from the path found
+ * add params from searchParams
+ *
+ * @param path
+ * @param params
+ */
+export function findMatch<T extends ObjectOf<RouteDefinition>>(
+ pagesMap: T,
+ pageList: Array<keyof T>,
+ path: string,
+ params: Record<string, string>,
+): Location<T> | undefined {
+ for (let idx = 0; idx < pageList.length; idx++) {
+ const name = pageList[idx];
+ const found = pagesMap[name].pattern.exec(path);
+ if (found !== null) {
+ const values = {} as Record<string, unknown>;
+
+ Object.entries(params).forEach(([key, value]) => {
+ values[key] = value;
+ });
+
+ if (found.groups !== undefined) {
+ Object.entries(found.groups).forEach(([key, value]) => {
+ values[key] = value;
+ });
+ }
+
+ // @ts-expect-error values is a map string which is equivalent to the RouteParamsType
+ return { name, parent: pagesMap, values };
+ }
+ }
+ return undefined;
+}
+
+/**
+ * get the type of the params of a location
+ *
+ */
+type RouteParamsType<
+ RouteType,
+ Key extends keyof RouteType,
+> = RouteType[Key] extends RouteDefinition<infer ParamType> ? ParamType : never;
+
+/**
+ * Helps to create a map of a type with the key
+ */
+type MapKeyValue<Type> = {
+ [Key in keyof Type]: Key extends string
+ ? {
+ parent: Type;
+ name: Key;
+ values: RouteParamsType<Type, Key>;
+ }
+ : never;
+};
+
+/**
+ * create a enumeration of value of a mapped type
+ */
+type EnumerationOf<T> = T[keyof T];
+
+export type Location<T> = EnumerationOf<MapKeyValue<T>>;