/* 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 */ 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; cancelRequest: (eventId: string) => void; }; // @ts-expect-error default value to undefined, should it be another thing? const ChallengerContext = createContext(undefined); export const useChallengerApiContext = (): ChallengerContextType => useContext(ChallengerContext); enum VersionHint { NONE, } type Evictors = { challenger?: CacheEvictor; } type ConfigResult = | 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>(); const { i18n } = useTranslationContext(); const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = buildChallengerApiClient(baseUrl, evictors); useEffect(() => { let keepRetrying = true; async function testConfig(): Promise { 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 { const httpFetch = new BrowserFetchHttpLib({ enableThrottling: true, requireTls: false, }); const tracker = new ActiviyTracker(); const httpLib = new ObservableHttpClientLibrary(httpFetch, { observe(ev) { tracker.notify(ev); }, }); const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger); async function getRemoteConfig(): Promise { 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, }); };