diff options
Diffstat (limited to 'packages/web-util/src/context')
-rw-r--r-- | packages/web-util/src/context/activity.ts | 76 | ||||
-rw-r--r-- | packages/web-util/src/context/api.ts | 49 | ||||
-rw-r--r-- | packages/web-util/src/context/bank-api.ts | 224 | ||||
-rw-r--r-- | packages/web-util/src/context/challenger-api.ts | 213 | ||||
-rw-r--r-- | packages/web-util/src/context/exchange-api.ts | 217 | ||||
-rw-r--r-- | packages/web-util/src/context/index.ts | 12 | ||||
-rw-r--r-- | packages/web-util/src/context/merchant-api.ts | 228 | ||||
-rw-r--r-- | packages/web-util/src/context/navigation.ts | 114 | ||||
-rw-r--r-- | packages/web-util/src/context/translation.ts | 119 | ||||
-rw-r--r-- | packages/web-util/src/context/wallet-integration.ts | 83 |
10 files changed, 1335 insertions, 0 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..d12d1efb6 --- /dev/null +++ b/packages/web-util/src/context/activity.ts @@ -0,0 +1,76 @@ +/* + 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 { ChallengerHttpClient, ObservabilityEvent, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerExchangeHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util"; + +type Listener<Event> = (e: Event) => void; +type Unsuscriber = () => void; +export type Subscriber<Event> = (fn: Listener<Event>) => Unsuscriber; + +export class ActiviyTracker<Event> { + private observers = new Array<Listener<Event>>(); + constructor() { + this.notify = this.notify.bind(this) + this.subscribe = this.subscribe.bind(this) + } + notify(data: Event): void { + 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: Subscriber<ObservabilityEvent>; + cancelRequest(id: string): void; +} + +export interface MerchantLib { + instance: TalerMerchantManagementHttpClient; + authenticate: TalerAuthenticationHttpClient; + subInstanceApi: (instanceId: string) => MerchantLib; +} + +export interface ExchangeLib { + exchange: TalerExchangeHttpClient; +} + +export interface BankLib { + bank: TalerCoreBankHttpClient; + conversion: TalerBankConversionHttpClient; + auth: (user: string) => TalerAuthenticationHttpClient; +} + +export interface ChallengerLib { + challenger: ChallengerHttpClient; +} + diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts new file mode 100644 index 000000000..c1eaa37f8 --- /dev/null +++ b/packages/web-util/src/context/api.ts @@ -0,0 +1,49 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util"; +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { defaultRequestHandler } from "../utils/request.js"; + +interface Type { + /** + * @deprecated this show not be used + */ + request: typeof defaultRequestHandler; + bankCore: TalerCoreBankHttpClient, + bankIntegration: TalerBankIntegrationHttpClient, + bankWire: TalerWireGatewayHttpClient, + bankRevenue: TalerRevenueHttpClient, +} + +const Context = createContext<Type>({ request: defaultRequestHandler } as any); + +export const useApiContext = (): Type => useContext(Context); +export const ApiContextProvider = ({ + children, + value, +}: { + value: Type; + children: ComponentChildren; +}): VNode => { + return h(Context.Provider, { value, children }); +}; 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..3f6a32f4b --- /dev/null +++ b/packages/web-util/src/context/bank-api.ts @@ -0,0 +1,224 @@ +/* + 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, Subscriber } 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: Subscriber<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 }; + +const CONFIG_FAIL_TRY_AGAIN_MS = 5000; + +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(() => { + let keepRetrying = true; + async function testConfig(): Promise<void> { + try { + const config = await getRemoteConfig(); + if (LibtoolVersion.compare(VERSION, config.version)) { + setChecked({ type: "ok", config, hints: [] }); + } else { + setChecked({ + type: "incompatible", + result: config, + supported: VERSION, + }); + } + } catch (error) { + if (error instanceof TalerError) { + if (keepRetrying) { + setTimeout(() => { + testConfig(); + }, CONFIG_FAIL_TRY_AGAIN_MS); + } + setChecked({ type: "error", error }); + } else { + setChecked({ type: "error", error: TalerError.fromException(error) }); + } + } + } + testConfig(); + return () => { + // on unload, stop retry + keepRetrying = false; + }; + }, []); + + 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, + httpLib, + ); + + async function getRemoteConfig(): Promise<TalerCorebankApi.Config> { + const resp = await bank.getConfig(); + if (resp.type === "fail") { + throw TalerError.fromUncheckedDetail(resp.detail); + } + 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/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts new file mode 100644 index 000000000..8748f5f69 --- /dev/null +++ b/packages/web-util/src/context/challenger-api.ts @@ -0,0 +1,213 @@ +/* + 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, + ChallengerApi, + ChallengerCacheEviction, + ChallengerHttpClient, + LibtoolVersion, + ObservabilityEvent, + ObservableHttpClientLibrary, + TalerError +} from "@gnu-taler/taler-util"; +import { + ComponentChildren, + FunctionComponent, + VNode, + createContext, + h, +} from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js"; +import { + APIClient, + ActiviyTracker, + ChallengerLib, + Subscriber +} from "./activity.js"; +import { useTranslationContext } from "./translation.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type ChallengerContextType = { + url: URL; + config: ChallengerApi.ChallengerTermsOfServiceResponse; + lib: ChallengerLib; + hints: VersionHint[]; + onActivity: Subscriber<ObservabilityEvent>; + cancelRequest: (eventId: string) => void; +}; + +// @ts-expect-error default value to undefined, should it be another thing? +const ChallengerContext = createContext<ChallengerContextType>(undefined); + +export const useChallengerApiContext = (): ChallengerContextType => + useContext(ChallengerContext); + +enum VersionHint { + NONE, +} + +type Evictors = { + challenger?: CacheEvictor<ChallengerCacheEviction>; +} + +type ConfigResult<T> = + | undefined + | { type: "ok"; config: T; hints: VersionHint[] } + | { type: "incompatible"; result: T; supported: string } + | { type: "error"; error: TalerError }; + +const CONFIG_FAIL_TRY_AGAIN_MS = 5000; + +export const ChallengerApiProvider = ({ + baseUrl, + children, + frameOnError, + evictors = {}, +}: { + baseUrl: URL; + children: ComponentChildren; + evictors?: Evictors; + frameOnError: FunctionComponent<{ children: ComponentChildren }>; +}): VNode => { + const [checked, setChecked] = + useState<ConfigResult<ChallengerApi.ChallengerTermsOfServiceResponse>>(); + const { i18n } = useTranslationContext(); + + const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = + buildChallengerApiClient(baseUrl, evictors); + + useEffect(() => { + let keepRetrying = true; + async function testConfig(): Promise<void> { + try { + const config = await getRemoteConfig(); + if (LibtoolVersion.compare(VERSION, config.version)) { + setChecked({ type: "ok", config, hints: [] }); + } else { + setChecked({ + type: "incompatible", + result: config, + supported: VERSION, + }); + } + } catch (error) { + if (error instanceof TalerError) { + if (keepRetrying) { + setTimeout(() => { + testConfig(); + }, CONFIG_FAIL_TRY_AGAIN_MS); + } + setChecked({ type: "error", error }); + } else { + setChecked({ type: "error", error: TalerError.fromException(error) }); + } + } + } + testConfig(); + return () => { + // on unload, stop retry + keepRetrying = false; + }; + }, []); + + 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: ChallengerContextType = { + url: baseUrl, + config: checked.config, + onActivity: onActivity, + lib, + cancelRequest, + hints: checked.hints, + }; + return h(ChallengerContext.Provider, { + value, + children, + }); +}; + +function buildChallengerApiClient( + url: URL, + evictors: Evictors, +): APIClient<ChallengerLib, ChallengerApi.ChallengerTermsOfServiceResponse> { + const httpFetch = new BrowserFetchHttpLib({ + enableThrottling: true, + requireTls: false, + }); + const tracker = new ActiviyTracker<ObservabilityEvent>(); + const httpLib = new ObservableHttpClientLibrary(httpFetch, { + observe(ev) { + tracker.notify(ev); + }, + }); + + const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger); + + async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> { + const resp = await challenger.getConfig(); + if (resp.type === "fail") { + throw TalerError.fromUncheckedDetail(resp.detail); + } + return resp.body; + } + + return { + getRemoteConfig, + VERSION: challenger.PROTOCOL_VERSION, + lib: { + challenger, + }, + onActivity: tracker.subscribe, + cancelRequest: httpLib.cancelRequest, + }; +} + +export const ChallengerApiProviderTesting = ({ + children, + value, +}: { + value: ChallengerContextType; + children: ComponentChildren; +}): VNode => { + return h(ChallengerContext.Provider, { + value, + children, + }); +}; diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts new file mode 100644 index 000000000..39f889ba9 --- /dev/null +++ b/packages/web-util/src/context/exchange-api.ts @@ -0,0 +1,217 @@ +/* + 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, + TalerError, + TalerExchangeApi, + TalerExchangeCacheEviction, + TalerExchangeHttpClient +} from "@gnu-taler/taler-util"; +import { + ComponentChildren, + FunctionComponent, + VNode, + createContext, + h, +} from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { BrowserFetchHttpLib, ErrorLoading, useTranslationContext } from "../index.browser.js"; +import { + APIClient, + ActiviyTracker, + ExchangeLib, + Subscriber, +} from "./activity.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type ExchangeContextType = { + url: URL; + config: TalerExchangeApi.ExchangeVersionResponse; + lib: ExchangeLib; + hints: VersionHint[]; + onActivity: Subscriber<ObservabilityEvent>; + cancelRequest: (eventId: string) => void; +}; + +// FIXME: below +// @ts-expect-error default value to undefined, should it be another thing? +const ExchangeContext = createContext<ExchangeContextType>(undefined); + +export const useExchangeApiContext = (): ExchangeContextType => + useContext(ExchangeContext); + +enum VersionHint { + NONE, +} + +type Evictors = { + exchange?: CacheEvictor<TalerExchangeCacheEviction>; +}; + +type ConfigResult<T> = + | undefined + | { type: "ok"; config: T; hints: VersionHint[] } + | ConfigResultFail<T>; + +type ConfigResultFail<T> = + | { type: "incompatible"; result: T; supported: string } + | { type: "error"; error: TalerError }; + +const CONFIG_FAIL_TRY_AGAIN_MS = 5000; + +export const ExchangeApiProvider = ({ + baseUrl, + children, + evictors = {}, + frameOnError, +}: { + baseUrl: URL; + evictors?: Evictors; + children: ComponentChildren; + frameOnError: FunctionComponent<{ children: ComponentChildren }>; +}): VNode => { + const [checked, setChecked] = + useState<ConfigResult<TalerExchangeApi.ExchangeVersionResponse>>(); + const { i18n } = useTranslationContext(); + + const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = + buildExchangeApiClient(baseUrl, evictors); + + useEffect(() => { + let keepRetrying = true; + async function testConfig(): Promise<void> { + try { + const config = await getRemoteConfig(); + if (LibtoolVersion.compare(VERSION, config.version)) { + setChecked({ type: "ok", config, hints: [] }); + } else { + setChecked({ + type: "incompatible", + result: config, + supported: VERSION, + }); + } + } catch (error) { + if (error instanceof TalerError) { + if (keepRetrying) { + setTimeout(() => { + testConfig(); + }, CONFIG_FAIL_TRY_AGAIN_MS); + } + setChecked({ type: "error", error }); + } else { + setChecked({ type: "error", error: TalerError.fromException(error) }); + } + } + } + testConfig(); + return () => { + // on unload, stop retry + keepRetrying = false; + }; + }, []); + + 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: ExchangeContextType = { + url: baseUrl, + config: checked.config, + onActivity: onActivity, + lib, + cancelRequest, + hints: checked.hints, + }; + return h(ExchangeContext.Provider, { + value, + children, + }); +}; + +function buildExchangeApiClient( + url: URL, + evictors: Evictors, +): APIClient<ExchangeLib, TalerExchangeApi.ExchangeVersionResponse> { + const httpFetch = new BrowserFetchHttpLib({ + enableThrottling: true, + requireTls: false, + }); + const tracker = new ActiviyTracker<ObservabilityEvent>(); + + const httpLib = new ObservableHttpClientLibrary(httpFetch, { + observe(ev) { + tracker.notify(ev); + }, + }); + + const ex = new TalerExchangeHttpClient(url.href, httpLib, evictors.exchange); + + async function getRemoteConfig(): Promise<TalerExchangeApi.ExchangeVersionResponse> { + const resp = await ex.getConfig(); + if (resp.type === "fail") { + throw TalerError.fromUncheckedDetail(resp.detail); + } + return resp.body; + } + + return { + getRemoteConfig, + VERSION: ex.PROTOCOL_VERSION, + lib: { + exchange: ex, + }, + onActivity: tracker.subscribe, + cancelRequest: httpLib.cancelRequest, + }; +} + +export const ExchangeApiProviderTesting = ({ + children, + value, +}: { + value: ExchangeContextType; + children: ComponentChildren; +}): VNode => { + return h(ExchangeContext.Provider, { + value, + children, + }); +}; diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts new file mode 100644 index 000000000..7e30ecd09 --- /dev/null +++ b/packages/web-util/src/context/index.ts @@ -0,0 +1,12 @@ +export { ApiContextProvider, useApiContext } from "./api.js"; +export { + InternationalizationAPI, + TranslationProvider, + useTranslationContext +} from "./translation.js"; +export * from "./bank-api.js"; +export * from "./challenger-api.js"; +export * from "./merchant-api.js"; +export * from "./exchange-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..03c95d48e --- /dev/null +++ b/packages/web-util/src/context/merchant-api.ts @@ -0,0 +1,228 @@ +/* + 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, + TalerMerchantManagementCacheEviction, + TalerMerchantManagementHttpClient, +} from "@gnu-taler/taler-util"; +import { + ComponentChildren, + FunctionComponent, + VNode, + createContext, + h, +} from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { BrowserFetchHttpLib } from "../index.browser.js"; +import { + APIClient, + ActiviyTracker, + MerchantLib, + Subscriber, +} from "./activity.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type MerchantContextType = { + url: URL; + config: TalerMerchantApi.VersionResponse; + lib: MerchantLib; + hints: VersionHint[]; + onActivity: Subscriber<ObservabilityEvent>; + cancelRequest: (eventId: string) => void; + changeBackend: (url: URL) => 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 | TalerMerchantInstanceCacheEviction + >; +}; + +type ConfigResult<T> = + | undefined + | { type: "ok"; config: T; hints: VersionHint[] } + | ConfigResultFail<T>; + +export type ConfigResultFail<T> = + | { type: "incompatible"; result: T; supported: string } + | { type: "error"; error: TalerError }; + +const CONFIG_FAIL_TRY_AGAIN_MS = 5000; + +export const MerchantApiProvider = ({ + baseUrl, + children, + evictors = {}, + frameOnError, +}: { + baseUrl: URL; + evictors?: Evictors; + children: ComponentChildren; + frameOnError: FunctionComponent<{ + state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined; + }>; +}): VNode => { + const [checked, setChecked] = + useState<ConfigResult<TalerMerchantApi.VersionResponse>>(); + + const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl); + + const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = + buildMerchantApiClient(merchantEndpoint, evictors); + + useEffect(() => { + let keepRetrying = true; + async function testConfig(): Promise<void> { + try { + const config = await getRemoteConfig(); + if (LibtoolVersion.compare(VERSION, config.version)) { + setChecked({ type: "ok", config, hints: [] }); + } else { + setChecked({ + type: "incompatible", + result: config, + supported: VERSION, + }); + } + } catch (error) { + if (error instanceof TalerError) { + if (keepRetrying) { + setTimeout(() => { + testConfig(); + }, CONFIG_FAIL_TRY_AGAIN_MS); + } + setChecked({ type: "error", error }); + } else { + setChecked({ type: "error", error: TalerError.fromException(error) }); + } + } + } + testConfig(); + return () => { + // on unload, stop retry + keepRetrying = false; + }; + }, []); + + if (!checked || checked.type !== "ok") { + return h(frameOnError, { state: checked }, []); + } + + const value: MerchantContextType = { + url: merchantEndpoint, + config: checked.config, + onActivity: onActivity, + lib, + cancelRequest, + changeBackend: changeMerchantEndpoint, + 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 instance = new TalerMerchantManagementHttpClient( + url.href, + httpLib, + evictors.management, + ); + const authenticate = new TalerAuthenticationHttpClient( + instance.getAuthenticationAPI().href, + httpLib, + ); + + function getSubInstanceAPI(instanceId: string): MerchantLib { + const api = buildMerchantApiClient( + instance.getSubInstanceAPI(instanceId) as URL, + evictors, + ); + return api.lib; + } + + async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> { + const resp = await instance.getConfig(); + if (resp.type === "fail") { + throw TalerError.fromUncheckedDetail(resp.detail); + } + return resp.body; + } + + return { + getRemoteConfig, + VERSION: instance.PROTOCOL_VERSION, + lib: { + instance, + authenticate, + subInstanceApi: getSubInstanceAPI, + }, + 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..c2f2bbbc1 --- /dev/null +++ b/packages/web-util/src/context/navigation.ts @@ -0,0 +1,114 @@ +/* + 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"; + +/** + * + * @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); + +// 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); +} + +function getPathAndParamsFromWindow(): { + path: string; + params: Record<string, string[]>; +} { + 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)) { + if (!params[key]) { + params[key] = []; + } + params[key].push(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): void { + const { params } = getPathAndParamsFromWindow(); + setState({ path, params }); + window.location.href = path; + } + + useEffect(() => { + function eventListener(): void { + 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/translation.ts b/packages/web-util/src/context/translation.ts new file mode 100644 index 000000000..2725dc7e1 --- /dev/null +++ b/packages/web-util/src/context/translation.ts @@ -0,0 +1,119 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { i18n, setupI18n } from "@gnu-taler/taler-util"; +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext, useEffect } from "preact/hooks"; +import { useLang } from "../hooks/index.js"; +import { Locale } from "date-fns"; +import { + es as esLocale, + enGB as enLocale, + fr as frLocale, + de as deLocale +} from "date-fns/locale" + +export type InternationalizationAPI = typeof i18n; + +interface Type { + lang: string; + supportedLang: { [id in keyof typeof supportedLang]: string }; + changeLanguage: (l: string) => void; + i18n: InternationalizationAPI; + dateLocale: Locale, + completeness: { [id in keyof typeof supportedLang]: number } +} + +const supportedLang = { + es: "Espanol [es]", + en: "English [en]", + fr: "Francais [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiane [it]", +}; + +const initial: Type = { + lang: "en", + supportedLang, + changeLanguage: () => { + // do not change anything + }, + i18n, + dateLocale: enLocale, + completeness: { + de: 0, + en: 0, + es: 0, + fr: 0, + it: 0, + sv: 0, + } +}; +const Context = createContext<Type>(initial); + +interface Props { + initial?: string; + children: ComponentChildren; + forceLang?: string; + source: Record<string, any>; + completeness?: Record<string, number>; +} + +// Outmost UI wrapper. +export const TranslationProvider = ({ + initial, + children, + forceLang, + source, + completeness: completenessProp +}: Props): VNode => { + const completeness = { + en: 100, + de: !completenessProp || !completenessProp["de"] ? 0 : completenessProp["de"], + es: !completenessProp || !completenessProp["es"] ? 0 : completenessProp["es"], + fr: !completenessProp || !completenessProp["fr"] ? 0 : completenessProp["fr"], + it: !completenessProp || !completenessProp["it"] ? 0 : completenessProp["it"], + sv: !completenessProp || !completenessProp["sv"] ? 0 : completenessProp["sv"], + } + const { value: lang, update: changeLanguage } = useLang(initial, completeness); + + useEffect(() => { + if (forceLang) { + changeLanguage(forceLang); + } + }); + useEffect(() => { + setupI18n(lang, source); + }, [lang]); + if (forceLang) { + setupI18n(forceLang, source); + } else { + setupI18n(lang, source); + } + + const dateLocale = lang === "es" ? esLocale : + lang === "fr" ? frLocale : + lang === "de" ? deLocale : + enLocale; + + return h(Context.Provider, { + value: { lang, changeLanguage, supportedLang, i18n, dateLocale, completeness }, + children, + }); +}; + +export const useTranslationContext = (): Type => useContext(Context); 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, + }); +}; |