/* 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, 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; 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(undefined); export const useMerchantApiContext = (): MerchantContextType => useContext(MerchantContext); enum VersionHint { NONE, } type Evictors = { management?: CacheEvictor< TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction >; }; type ConfigResult = | undefined | { type: "ok"; config: T; hints: VersionHint[] } | ConfigResultFail; export type ConfigResultFail = | { 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 | undefined; }>; }): VNode => { const [checked, setChecked] = useState>(); const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl); const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = buildMerchantApiClient(merchantEndpoint, 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 || 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 { const httpFetch = new BrowserFetchHttpLib({ enableThrottling: true, requireTls: false, }); const tracker = new ActiviyTracker(); 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 { 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, }); };