taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b58d53dd93bd8e97aecc28fae788c5c7051fd73d
parent 31cf3187e447e2c4ec8a473362c5bacc07a874f1
Author: Sebastian <sebasjm@gmail.com>
Date:   Sun,  5 Nov 2023 18:04:22 -0300

sharing components in web-util

Diffstat:
Mpackages/aml-backoffice-ui/src/App.tsx | 11+++++++++--
Mpackages/aml-backoffice-ui/src/Dashboard.tsx | 26+++++++++++++++-----------
Dpackages/aml-backoffice-ui/src/account.ts | 128-------------------------------------------------------------------------------
Apackages/aml-backoffice-ui/src/context/config.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/handlers/Caption.tsx | 7++-----
Mpackages/aml-backoffice-ui/src/hooks/useBackend.ts | 27++++++++++++++++++---------
Mpackages/aml-backoffice-ui/src/hooks/useCaseDetails.ts | 103++++++++++++++++---------------------------------------------------------------
Mpackages/aml-backoffice-ui/src/hooks/useCases.ts | 126+++++++++++++++++++++++++++++++++++--------------------------------------------
Mpackages/aml-backoffice-ui/src/hooks/useOfficer.ts | 20+++++++++-----------
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 83++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 46+++++++++++++++++++++++++---------------------
Mpackages/aml-backoffice-ui/src/pages/NewFormEntry.tsx | 19+++++++++----------
Mpackages/aml-backoffice-ui/src/pages/Officer.tsx | 4++--
Mpackages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 3+--
Dpackages/aml-backoffice-ui/src/utils/errors.tsx | 77-----------------------------------------------------------------------------
Dpackages/demobank-ui/src/components/Attention.tsx | 59-----------------------------------------------------------
Mpackages/demobank-ui/src/components/Cashouts/index.ts | 2+-
Mpackages/demobank-ui/src/components/Cashouts/views.tsx | 5++---
Dpackages/demobank-ui/src/components/CopyButton.tsx | 48------------------------------------------------
Mpackages/demobank-ui/src/components/EmptyComponentExample/index.ts | 5+----
Dpackages/demobank-ui/src/components/ErrorLoading.tsx | 120-------------------------------------------------------------------------------
Dpackages/demobank-ui/src/components/LangSelector.tsx | 111-------------------------------------------------------------------------------
Mpackages/demobank-ui/src/components/Routing.tsx | 4----
Dpackages/demobank-ui/src/components/ShowLocalNotification.tsx | 43-------------------------------------------
Mpackages/demobank-ui/src/components/Transactions/index.ts | 5+----
Mpackages/demobank-ui/src/components/app.tsx | 1+
Mpackages/demobank-ui/src/context/config.ts | 2+-
Mpackages/demobank-ui/src/pages/AccountPage/index.ts | 4++--
Mpackages/demobank-ui/src/pages/AccountPage/views.tsx | 2+-
Mpackages/demobank-ui/src/pages/BankFrame.tsx | 10+++-------
Mpackages/demobank-ui/src/pages/LoginForm.tsx | 6+++---
Mpackages/demobank-ui/src/pages/OperationState/index.ts | 4++--
Mpackages/demobank-ui/src/pages/OperationState/views.tsx | 8++++----
Mpackages/demobank-ui/src/pages/PaytoWireTransferForm.tsx | 4++--
Mpackages/demobank-ui/src/pages/PublicHistoriesPage.tsx | 2+-
Mpackages/demobank-ui/src/pages/QrCodeSection.tsx | 2+-
Mpackages/demobank-ui/src/pages/RegistrationPage.tsx | 4++--
Mpackages/demobank-ui/src/pages/ShowAccountDetails.tsx | 6+++---
Mpackages/demobank-ui/src/pages/UpdateAccountPassword.tsx | 8++++----
Mpackages/demobank-ui/src/pages/WalletWithdrawForm.tsx | 4++--
Mpackages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 4++--
Mpackages/demobank-ui/src/pages/WithdrawalOperationPage.tsx | 2+-
Mpackages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 6+++---
Mpackages/demobank-ui/src/pages/admin/Account.tsx | 4++--
Mpackages/demobank-ui/src/pages/admin/AccountForm.tsx | 4++--
Mpackages/demobank-ui/src/pages/admin/AccountList.tsx | 4++--
Mpackages/demobank-ui/src/pages/admin/AdminHome.tsx | 2+-
Mpackages/demobank-ui/src/pages/admin/CreateNewAccount.tsx | 4++--
Mpackages/demobank-ui/src/pages/admin/RemoveAccount.tsx | 10+++++-----
Mpackages/demobank-ui/src/pages/business/CreateCashout.tsx | 10+++++-----
Mpackages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx | 10+++++-----
Mpackages/demobank-ui/tailwind.config.js | 8+++++++-
Rpackages/demobank-ui/src/assets/lang.svg -> packages/web-util/src/assets/lang.svg | 0
Apackages/web-util/src/components/Attention.tsx | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/components/CopyButton.tsx | 46++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/components/ErrorLoading.tsx | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/components/LangSelector.tsx | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rpackages/demobank-ui/src/components/Loading.tsx -> packages/web-util/src/components/Loading.tsx | 0
Rpackages/demobank-ui/src/components/ShowInputErrorLabel.tsx -> packages/web-util/src/components/ShowInputErrorLabel.tsx | 0
Apackages/web-util/src/components/ShowLocalNotification.tsx | 43+++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/components/index.ts | 7+++++++
Apackages/web-util/src/declaration.d.ts | 35+++++++++++++++++++++++++++++++++++
62 files changed, 790 insertions(+), 935 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx @@ -1,12 +1,19 @@ import { TranslationProvider } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { Dashboard } from "./Dashboard.js"; +import { ExchangeAmlFrame, Main } from "./Dashboard.js"; import "./scss/main.css"; +import { ExchangeApiProvider } from "./context/config.js"; +import { getInitialBackendBaseURL } from "./hooks/useBackend.js"; export function App(): VNode { + const baseUrl = getInitialBackendBaseURL(); return ( <TranslationProvider source={{}}> - <Dashboard /> + <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}> + <ExchangeAmlFrame> + <Main /> + </ExchangeAmlFrame> + </ExchangeApiProvider> </TranslationProvider> ); } diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx @@ -182,7 +182,7 @@ function LeftMenu() { ); } -export function Dashboard({ +export function ExchangeAmlFrame({ children, }: { children?: ComponentChildren; @@ -211,21 +211,25 @@ export function Dashboard({ }} /> <Notifications /> - <main class="py-10 px-4 sm:px-6 lg:px-8"> - <div class="mx-auto max-w-3xl"> - <Router - pageList={pageList} - onNotFound={() => { - return <div>not found</div>; - }} - /> - </div> - </main> + {children} </div> </Fragment> ); } +export function Main(): VNode { + return <main class="py-10 px-4 sm:px-6 lg:px-8"> + <div class="mx-auto max-w-3xl"> + <Router + pageList={pageList} + onNotFound={() => { + return <div>not found</div>; + }} + /> + </div> + </main> +} + const pageList = Object.values(Pages); function NavigationBar({ diff --git a/packages/aml-backoffice-ui/src/account.ts b/packages/aml-backoffice-ui/src/account.ts @@ -1,128 +0,0 @@ -import { - Amounts, - TalerSignaturePurpose, - amountToBuffer, - bufferForUint32, - buildSigPS, - createEddsaKeyPair, - decodeCrock, - decryptWithDerivedKey, - eddsaGetPublic, - eddsaSign, - encodeCrock, - encryptWithDerivedKey, - getRandomBytesF, - hash, - hashTruncate32, - stringToBytes, - timestampRoundedToBuffer -} from "@gnu-taler/taler-util"; -import { AmlExchangeBackend } from "./types.js"; - -export interface Account { - accountId: AccountId; - signingKey: SigningKey; -} - -/** - * Restore previous session and unlock account with password - * - * @param salt string from which crypto params will be derived - * @param key secured private key - * @param password password for the private key - * @returns - */ -export async function unlockAccount( - account: LockedAccount, - password: string, -): Promise<Account> { - const rawKey = decodeCrock(account); - const rawPassword = stringToBytes(password); - - const signingKey = (await decryptWithDerivedKey( - rawKey, - rawPassword, - password, - ).catch((e: Error) => { - throw new UnwrapKeyError(e.message); - })) as SigningKey; - - const publicKey = eddsaGetPublic(signingKey); - - const accountId = encodeCrock(publicKey) as AccountId; - - return { accountId, signingKey }; -} - -export function buildQuerySignature(key: SigningKey): string { - const sigBlob = buildSigPS( - TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY, - ).build(); - - return encodeCrock(eddsaSign(sigBlob, key)); -} - -export function buildDecisionSignature( - key: SigningKey, - decision: AmlExchangeBackend.AmlDecision, -): string { - const zero = new Uint8Array(new ArrayBuffer(64)) - - const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) - //TODO: new need the null terminator, also in the exchange - .put(hash(stringToBytes(decision.justification)))//check null - .put(timestampRoundedToBuffer(decision.decision_time)) - .put(amountToBuffer(decision.new_threshold)) - .put(decodeCrock(decision.h_payto)) - .put(zero) //kyc_requirement - .put(bufferForUint32(decision.new_state)) - .build(); - - return encodeCrock(eddsaSign(sigBlob, key)); -} - -declare const opaque_Account: unique symbol; -export type LockedAccount = string & { [opaque_Account]: true }; - -declare const opaque_AccountId: unique symbol; -export type AccountId = string & { [opaque_AccountId]: true }; - -declare const opaque_SigningKey: unique symbol; -export type SigningKey = Uint8Array & { [opaque_SigningKey]: true }; - -/** - * Create new account (secured private key) - * secured with the given password - * - * @param sessionId - * @param password - * @returns - */ -export async function createNewAccount( - password: string, -): Promise<Account & { safe: LockedAccount }> { - const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); - - const key = stringToBytes(password); - - const protectedPrivKey = await encryptWithDerivedKey( - getRandomBytesF(24), - key, - eddsaPriv, - password, - ); - - const signingKey = eddsaPriv as SigningKey; - const accountId = encodeCrock(eddsaPub) as AccountId; - const safe = encodeCrock(protectedPrivKey) as LockedAccount; - - return { accountId, signingKey, safe }; -} - -export class UnwrapKeyError extends Error { - public cause: string; - constructor(cause: string) { - super(`Recovering private key failed on: ${cause}`); - this.cause = cause; - } -} diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts @@ -0,0 +1,89 @@ +/* + 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 { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util"; +import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type Type = { + url: URL, + config: TalerExchangeApi.ExchangeVersionResponse, + api: TalerExchangeHttpClient, +}; + +const Context = createContext<Type>(undefined as any); + +export const useExchangeApiContext = (): Type => useContext(Context); + +export type ConfigResult = undefined + | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse } + | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string } + | { type: "error", error: TalerError } + +export const ExchangeApiProvider = ({ + baseUrl, + children, + frameOnError, +}: { + baseUrl: string, + children: ComponentChildren; + frameOnError: FunctionComponent<{ children: ComponentChildren }>, +}): VNode => { + const [checked, setChecked] = useState<ConfigResult>() + const { i18n } = useTranslationContext(); + const url = new URL(baseUrl) + const api = new TalerExchangeHttpClient(url.href, new BrowserHttpLib()) + useEffect(() => { + api.getConfig() + .then((resp) => { + if (api.isCompatible(resp.body.version)) { + setChecked({ type: "ok", config: resp.body }); + } else { + setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION }) + } + }) + .catch((error: unknown) => { + if (error instanceof TalerError) { + setChecked({ type: "error", error }); + } + }); + }, []); + + if (checked === undefined) { + return h(frameOnError, { children: h("div", {}, "loading...") }) + } + 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 bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) }) + } + const value: Type = { + url, config: checked.config, api + } + return h(Context.Provider, { + value, + children, + }); +}; + diff --git a/packages/aml-backoffice-ui/src/handlers/Caption.tsx b/packages/aml-backoffice-ui/src/handlers/Caption.tsx @@ -1,11 +1,8 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { - IconAddon, - InputLine, - LabelWithTooltipMaybeRequired, - UIFormProps, + LabelWithTooltipMaybeRequired } from "./InputLine.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; interface Props { label: TranslatedString; diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts @@ -1,3 +1,4 @@ +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { HttpResponseOk, RequestOptions, @@ -5,9 +6,6 @@ import { } from "@gnu-taler/web-util/browser"; import { useCallback } from "preact/hooks"; import { uiSettings } from "../settings.js"; -import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; -import { useOfficer } from "./useOfficer.js"; -import { buildQuerySignature } from "../account.js"; interface useBackendType { request: <T>( @@ -35,7 +33,7 @@ export function usePublicBackend(): useBackendType { ); const fetcher = useCallback( - function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string,string]): Promise<HttpResponseOk<T>> { + function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string, string]): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { talerAmlOfficerSignature }); @@ -66,18 +64,29 @@ export function usePublicBackend(): useBackendType { export function getInitialBackendBaseURL(): string { const overrideUrl = typeof localStorage !== "undefined" - ? localStorage.getItem("exchange-aml-base-url") + ? localStorage.getItem("exchange-base-url") : undefined; + + let result: string; + if (!overrideUrl) { //normal path if (!uiSettings.backendBaseURL) { console.error( "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); - return canonicalizeBaseUrl(window.origin); + result = window.origin + } else { + result = uiSettings.backendBaseURL; } - return canonicalizeBaseUrl(uiSettings.backendBaseURL); + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) } - // testing/development path - return canonicalizeBaseUrl(overrideUrl); } diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts @@ -1,34 +1,29 @@ import { HttpResponse, - HttpResponseOk, - RequestError + HttpResponseOk } from "@gnu-taler/web-util/browser"; import { AmlExchangeBackend } from "../types.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, useSWRConfig } from "swr"; -import { AccountId } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; import { usePublicBackend } from "./useBackend.js"; +import { useOfficer } from "./useOfficer.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useCaseDetails( - account: AccountId, - paytoHash: string, - signature: string | undefined, -): HttpResponse< - AmlExchangeBackend.AmlDecisionDetails, - AmlExchangeBackend.AmlError -> { - const { fetcher } = usePublicBackend(); +export function useCaseDetails(paytoHash: string) { + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; - const { data, error } = useSWR< - HttpResponseOk<AmlExchangeBackend.AmlDecisionDetails>, - RequestError<AmlExchangeBackend.AmlError> ->( [ - `aml/${account}/decision/${(paytoHash)}`, - signature, -], -fetcher, { + const { api } = useExchangeApiContext(); + + async function fetcher([officer, account]: [OfficerAccount, PaytoString]) { + return await api.getDecisionDetails(officer, account) + } + + const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>( + !session ? undefined : [session, paytoHash], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -41,11 +36,11 @@ fetcher, { }); if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } -const example1: AmlExchangeBackend.AmlDecisionDetails = { +const example1: TalerExchangeApi.AmlDecisionDetails = { aml_history: [ { justification: "Lack of documentation", @@ -54,7 +49,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000, }, new_state: 2, - new_threshold: "USD:0", + new_threshold: "USD:0" as AmountString, }, { justification: "Doing a transfer of high amount", @@ -63,7 +58,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6, }, new_state: 1, - new_threshold: "USD:2000", + new_threshold: "USD:2000" as AmountString, }, { justification: "Account is known to the system", @@ -72,7 +67,7 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9, }, new_state: 0, - new_threshold: "USD:100", + new_threshold: "USD:100" as AmountString, }, ], kyc_attributes: [ @@ -103,60 +98,4 @@ const example1: AmlExchangeBackend.AmlDecisionDetails = { ], }; -export const exampleResponse: HttpResponse<AmlExchangeBackend.AmlDecisionDetails,AmlExchangeBackend.AmlError> = { - ok: true, - data: example1, -} - - -export function useAmlCasesAPI(): AmlCaseAPI { - const { request } = usePublicBackend(); - const mutateAll = useMatchMutate(); - - const updateDecision = async ( - officer: AccountId, - data: AmlExchangeBackend.AmlDecision, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`aml/${officer}/decision`, { - method: "POST", - data, - contentType: "json", - }); - await mutateAll(/.*aml.*/); - return res; - }; - - return { - updateDecision, - }; -} - -export interface AmlCaseAPI { - updateDecision: ( - officer: AccountId, - data: AmlExchangeBackend.AmlDecision, - ) => Promise<HttpResponseOk<void>>; -} - -function useMatchMutate(): ( - re: RegExp, - value?: unknown, -) => Promise<any> { - const { cache, mutate } = useSWRConfig(); - - if (!(cache instanceof Map)) { - throw new Error( - "matchMutate requires the cache provider to be a Map instance", - ); - } - - return function matchRegexMutate(re: RegExp, value?: unknown) { - const allKeys = Array.from(cache.keys()); - const keys = allKeys.filter((key) => re.test(key)); - const mutations = keys.map((key) => { - return mutate(key, value, true); - }); - return Promise.all(mutations); - }; -} diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts @@ -1,16 +1,13 @@ -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; -import { AmlExchangeBackend } from "../types.js"; import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + HttpResponsePaginated } from "@gnu-taler/web-util/browser"; +import { AmlExchangeBackend } from "../types.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AmountString, OfficerAccount, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook } from "swr"; -import { usePublicBackend } from "./useBackend.js"; -import { AccountId, buildQuerySignature } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; import { useOfficer } from "./useOfficer.js"; const useSWR = _useSWR as unknown as SWRHook; @@ -22,59 +19,49 @@ const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; * @param args * @returns */ -export function useCases( - account: AccountId, - state: AmlExchangeBackend.AmlState, - signature: string | undefined, -): HttpResponsePaginated< - AmlExchangeBackend.AmlRecords, - AmlExchangeBackend.AmlError -> { - const { paginatedFetcher } = usePublicBackend(); +export function useCases(state: AmlExchangeBackend.AmlState) { + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; + const { api } = useExchangeApiContext(); - const [page, setPage] = useState(1); + const [offset, setOffet] = useState<string>(); + + async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) { + return await api.getDecisionsByState(officer, state, { + order: "asc", offset, limit: MAX_RESULT_SIZE + }) + } - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<AmlExchangeBackend.AmlRecords>, - RequestError<AmlExchangeBackend.AmlError> - >( - [ - `aml/${account}/decisions/${AmlExchangeBackend.AmlState[state]}`, - page, - PAGE_SIZE, - signature, - ], - paginatedFetcher, + const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>( + !session ? undefined : [session, state, offset], + fetcher, ); - const [lastAfter, setLastAfter] = useState< - HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError> - >({ loading: true }); + // const [lastAfter, setLastAfter] = useState< + // HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError> + // >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); + // useEffect(() => { + // if (afterData) setLastAfter(afterData); + // }, [afterData]); - if (afterError) { - return afterError.cause; - } + // if (afterError) { + // return afterError.cause; + // } // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data && afterData.data.records.length < PAGE_SIZE; - const isReachingStart = false; + const isLastPage = + data && data.type === "ok" && data.body.records.length < PAGE_SIZE; + const isFirstPage = !offset; const pagination = { - isReachingEnd, - isReachingStart, + isLastPage, + isFirstPage, loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data && afterData.data.records.length < MAX_RESULT_SIZE) { - setPage(page + 1); + if (isLastPage || data?.type !== "ok") return; + const list = data.body.records + if (list.length < MAX_RESULT_SIZE) { + // setOffset(list[list.length-1].account_name); } }, loadMorePrev: () => { @@ -82,65 +69,62 @@ export function useCases( }, }; - const records = !afterData - ? [] - : ((afterData ?? lastAfter).data ?? { records: [] }).records; - if (loadingAfter) return { loading: true, data: { records } }; - if (afterData) { - return { ok: true, data: { records }, ...pagination }; + // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts; + if (data) { + if (data.type === "fail") { + return { data } + } + return { data, pagination } + } + if (error) { + return error; } - return { loading: true }; + return undefined; } -const example1: AmlExchangeBackend.AmlRecords = { +const example1: TalerExchangeApi.AmlRecords = { records: [ { current_state: 0, h_payto: "QWEQWEQWEQWEWQE", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 1, h_payto: "ASDASDASD", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 2, h_payto: "ZXCZXCZXCXZC", rowid: 1, - threshold: "USD 1000", + threshold: "USD 1000" as AmountString, }, { current_state: 0, h_payto: "QWEQWEQWEQWEWQE", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 1, h_payto: "ASDASDASD", rowid: 1, - threshold: "USD 100", + threshold: "USD 100" as AmountString, }, { current_state: 2, h_payto: "ZXCZXCZXCXZC", rowid: 1, - threshold: "USD 1000", + threshold: "USD 1000" as AmountString, }, ].map((e, idx) => { e.rowid = idx; - e.threshold = `${e.threshold}${idx}`; + e.threshold = `${e.threshold}${idx}` as AmountString; return e; }), }; -export const exampleResponse: HttpResponsePaginated<AmlExchangeBackend.AmlRecords,AmlExchangeBackend.AmlError> = { - ok: true, - data: example1, - loadMore: () => {}, - loadMorePrev: () => {}, -} diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/useOfficer.ts @@ -1,17 +1,15 @@ import { AbsoluteTime, Codec, + LockedAccount, + OfficerAccount, buildCodecForObject, codecForAbsoluteTime, codecForString, + createNewOfficerAccount, + unlockOfficerAccount, } from "@gnu-taler/taler-util"; import { - Account, - LockedAccount, - createNewAccount, - unlockAccount, -} from "../account.js"; -import { buildStorageKey, useLocalStorage, useMemoryStorage, @@ -43,7 +41,7 @@ interface OfficerLocked { } interface OfficerReady { state: "ready"; - account: Account; + account: OfficerAccount; forget: () => void; lock: () => void; } @@ -52,7 +50,7 @@ const OFFICER_KEY = buildStorageKey("officer", codecForOfficer()); const ACCOUNT_KEY = "account"; export function useOfficer(): OfficerState { - const accountStorage = useMemoryStorage<Account>(ACCOUNT_KEY); + const accountStorage = useMemoryStorage<OfficerAccount>(ACCOUNT_KEY); const officerStorage = useLocalStorage(OFFICER_KEY); const officer = officerStorage.value; @@ -62,13 +60,13 @@ export function useOfficer(): OfficerState { return { state: "not-found", create: async (pwd: string) => { - const { accountId, safe, signingKey } = await createNewAccount(pwd); + const { id, safe, signingKey } = await createNewOfficerAccount(pwd); officerStorage.update({ account: safe, when: AbsoluteTime.now(), }); - accountStorage.update({ accountId, signingKey }); + accountStorage.update({ id, signingKey }); }, }; } @@ -80,7 +78,7 @@ export function useOfficer(): OfficerState { officerStorage.reset(); }, tryUnlock: async (pwd: string) => { - const ac = await unlockAccount(officer.account, pwd); + const ac = await unlockOfficerAccount(officer.account, pwd); accountStorage.update(ac); }, }; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -1,24 +1,23 @@ -import { Fragment, VNode, h } from "preact"; import { AbsoluteTime, AmountJson, Amounts, + PaytoString, + TalerError, TranslatedString, + assertUnreachable, } from "@gnu-taler/taler-util"; -import { format } from "date-fns"; +import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid"; +import { format } from "date-fns"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NiceForm } from "../NiceForm.js"; import { FlexibleForm } from "../forms/index.js"; import { UIFormField } from "../handlers/forms.js"; +import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { useOfficer } from "../hooks/useOfficer.js"; -import { buildQuerySignature } from "../account.js"; -import { useCaseDetails } from "../hooks/useCaseDetails.js"; -import { handleNotOkResult } from "../utils/errors.js"; type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent; type AmlFormEvent = { @@ -85,30 +84,33 @@ function getEventsFromAmlHistory( return ae.concat(ke).sort(selectSooner); } -export function CaseDetails({ account: paytoHash }: { account: string }) { +export function CaseDetails({ account }: { account: string }) { const [selected, setSelected] = useState<AmlEvent | undefined>(undefined); - const officer = useOfficer(); const { i18n } = useTranslationContext(); - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; + const details = useCaseDetails(account) + if (!details) { + return <Loading /> } - const signature = - officer.state === "ready" - ? buildQuerySignature(officer.account.signingKey) - : undefined; - const details = useCaseDetails(officer.account.accountId, paytoHash, signature) - if (!details.ok && !details.loading) { - return handleNotOkResult(i18n)(details); + if (details instanceof TalerError) { + return <ErrorLoading error={details} /> } - const aml_history = details.loading ? [] : details.data.aml_history - const kyc_attributes = details.loading ? [] : details.data.kyc_attributes - const events = getEventsFromAmlHistory(aml_history,kyc_attributes); - + if (details.type === "fail") { + switch (details.case) { + case "unauthorized": + case "officer-not-found": + case "officer-disabled": return <div /> + default: assertUnreachable(details) + } + } + const { aml_history, kyc_attributes } = details.body + + const events = getEventsFromAmlHistory(aml_history, kyc_attributes); + return ( <div> <a - href={Pages.newFormEntry.url({ account: paytoHash })} + href={Pages.newFormEntry.url({ account })} class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > New AML form @@ -287,23 +289,22 @@ function ShowConsolidated({ }, Object.entries(cons.kyc).length > 0 ? { - title: "KYC" as TranslatedString, - fields: Object.entries(cons.kyc).map(([key, field]) => { - const result: UIFormField = { - type: "text", - props: { - label: key as TranslatedString, - name: `kyc.${key}.value`, - help: `${field.provider} since ${ - field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy") + title: "KYC" as TranslatedString, + fields: Object.entries(cons.kyc).map(([key, field]) => { + const result: UIFormField = { + type: "text", + props: { + label: key as TranslatedString, + name: `kyc.${key}.value`, + help: `${field.provider} since ${field.since.t_ms === "never" + ? "never" + : format(field.since.t_ms, "dd/MM/yyyy") }` as TranslatedString, - }, - }; - return result; - }), - } + }, + }; + return result; + }), + } : undefined, ], }; @@ -319,7 +320,7 @@ function ShowConsolidated({ key={`${String(Date.now())}`} form={form} initial={cons} - onUpdate={() => {}} + onUpdate={() => { }} /> </Fragment> ); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -1,4 +1,5 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; +import { TalerError, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { createNewForm } from "../handlers/forms.js"; @@ -7,34 +8,37 @@ import { useOfficer } from "../hooks/useOfficer.js"; import { Pages } from "../pages.js"; import { AmlExchangeBackend } from "../types.js"; import { amlStateConverter } from "./CaseDetails.js"; -import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { buildQuerySignature } from "../account.js"; -import { handleNotOkResult } from "../utils/errors.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; export function Cases() { - const officer = useOfficer(); const { i18n } = useTranslationContext(); - if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; - } - const form = createNewForm<{ - state: AmlExchangeBackend.AmlState; - }>(); - const signature = - officer.state === "ready" - ? buildQuerySignature(officer.account.signingKey) - : undefined; + const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>(); + const initial = AmlExchangeBackend.AmlState.pending; const [stateFilter, setStateFilter] = useState(initial); - const list = useCases(officer.account.accountId, stateFilter, signature); - if (!list.ok && !list.loading) { - return handleNotOkResult(i18n)(list); + const list = useCases(stateFilter); + + if (!list) { + return <Loading /> + } + + if (list instanceof TalerError) { + return <ErrorLoading error={list} /> } - const records = list.loading ? [] : list.data.records + + if (list.data.type === "fail") { + switch (list.data.case) { + case "unauthorized": + case "officer-not-found": + case "officer-disabled": return <div /> + default: assertUnreachable(list.data) + } + } + + const { records } = list.data.body + return ( <div> <div class="px-4 sm:px-6 lg:px-8"> @@ -52,7 +56,7 @@ export function Cases() { onUpdate={(v) => { setStateFilter(v.state ?? initial); }} - onSubmit={(v) => {}} + onSubmit={(v) => { }} > <form.InputChoiceHorizontal name="state" diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -2,12 +2,11 @@ import { VNode, h } from "preact"; import { allForms } from "./AntiMoneyLaunderingForm.js"; import { Pages } from "../pages.js"; import { NiceForm } from "../NiceForm.js"; -import { AbsoluteTime, Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amounts, TalerExchangeApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { AmlExchangeBackend } from "../types.js"; -import { useAmlCasesAPI } from "../hooks/useCaseDetails.js"; import { useOfficer } from "../hooks/useOfficer.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; -import { buildDecisionSignature, buildQuerySignature } from "../account.js"; +import { useExchangeApiContext } from "../context/config.js"; export function NewFormEntry({ account, @@ -40,21 +39,21 @@ export function NewFormEntry({ state: AmlExchangeBackend.AmlState.pending, threshold: Amounts.parseOrThrow("KUDOS:1000"), }; - const api = useAmlCasesAPI() - + const { api } = useExchangeApiContext() + return ( <NiceForm initial={initial} form={showingFrom(initial)} onSubmit={(formValue) => { if (formValue.state === undefined || formValue.threshold === undefined) return; - + const justification = { index: selectedForm, name: formName, value: formValue } - const decision: AmlExchangeBackend.AmlDecision = { + const decision: TalerExchangeApi.AmlDecision = { justification: JSON.stringify(justification), decision_time: TalerProtocolTimestamp.now(), h_payto: account, @@ -63,9 +62,9 @@ export function NewFormEntry({ officer_sig: "", kyc_requirements: undefined } - const signature = buildDecisionSignature(officer.account.signingKey, decision); - decision.officer_sig = signature - api.updateDecision(officer.account.accountId, decision); + // const signature = buildDecisionSignature(officer.account.signingKey, decision); + // decision.officer_sig = signature + api.addDecisionDetails(officer.account, decision); // alert(JSON.stringify(formValue)); }} diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx @@ -14,12 +14,12 @@ export function Officer() { Public key </h1> <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg"> - <p class="mt-6 font-mono break-all">{officer.account.accountId}</p> + <p class="mt-6 font-mono break-all">{officer.account.id}</p> </div> <p> <a href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent( - `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`, + `I want my AML account\n\n\nPubKey: ${officer.account.id}`, )}`} target="_blank" rel="noreferrer" diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -1,7 +1,6 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; +import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util"; import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { UnwrapKeyError } from "../account.js"; import { createNewForm } from "../handlers/forms.js"; export function UnlockAccount({ diff --git a/packages/aml-backoffice-ui/src/utils/errors.tsx b/packages/aml-backoffice-ui/src/utils/errors.tsx @@ -1,77 +0,0 @@ -import { - ErrorType, - HttpResponse, - HttpResponsePaginated, - notifyError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { Loading } from "./Loading.js"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { AmlExchangeBackend } from "../types.js"; - -export function handleNotOkResult<Error extends AmlExchangeBackend.AmlError>( - i18n: ReturnType<typeof useTranslationContext>["i18n"], -): <T>( - result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>, -) => VNode { - return function handleNotOkResult2<T>( - result: HttpResponsePaginated<T, Error> | HttpResponse<T, Error>, - ): VNode { - if (result.loading) return <Loading />; - if (!result.ok) { - switch (result.type) { - case ErrorType.TIMEOUT: { - notifyError(i18n.str`Request timeout, try again later.`, undefined); - break; - } - case ErrorType.CLIENT: { - if (result.status === HttpStatusCode.Unauthorized) { - notifyError(i18n.str`Wrong credentials`, undefined); - return <div> not authorized</div>; - } - const errorData = result.payload; - notifyError( - i18n.str`Could not load due to a client error`, - errorData.hint as TranslatedString, - JSON.stringify(result), - ); - break; - } - case ErrorType.SERVER: { - notifyError( - i18n.str`Server returned with error`, - result.payload.hint as TranslatedString, - JSON.stringify(result.payload), - ); - break; - } - case ErrorType.UNREADABLE: { - notifyError( - i18n.str`Unexpected error.`, - `Response from ${result.info?.url} is unreadable, http status: ${result.status}` as TranslatedString, - JSON.stringify(result), - ); - break; - } - case ErrorType.UNEXPECTED: { - notifyError( - i18n.str`Unexpected error.`, - `Diagnostic from ${result.info?.url} is "${result.message}"` as TranslatedString, - JSON.stringify(result), - ); - break; - } - default: { - assertUnreachable(result); - } - } - - return <div>error</div>; - } - return <div />; - }; -} -export function assertUnreachable(x: never): never { - throw new Error("Didn't expect to get here"); -} diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx @@ -1,59 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { assertUnreachable } from "./Routing.js"; - -interface Props { - type?: "info" | "success" | "warning" | "danger", - onClose?: () => void, - title: TranslatedString, - children?: ComponentChildren , -} -export function Attention({ type = "info", title, children, onClose }: Props): VNode { - return <div class={`group attention-${type} mt-2 shadow-lg`}> - <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> - <div class="flex"> - <div > - <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400"> - {(() => { - switch (type) { - case "info": - return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" /> - case "warning": - return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> - case "danger": - return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> - case "success": - return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /> - default: - assertUnreachable(type) - } - })()} - </svg> - </div> - <div class="ml-3 w-full"> - <h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800"> - {title} - </h3> - <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700"> - {children} - </div> - </div> - {onClose && - <div> - <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" - onClick={(e) => { - e.preventDefault(); - onClose(); - }} - > - <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> - </svg> - </button> - </div> - } - </div> - </div> - - </div> -} diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts @@ -15,7 +15,7 @@ */ import { HttpError, utils } from "@gnu-taler/web-util/browser"; -import { Loading } from "../Loading.js"; +import { Loading } from "@gnu-taler/web-util/browser"; // import { compose, StateViewMap } from "../../utils/index.js"; // import { wxApi } from "../../wxApi.js"; import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -18,10 +18,9 @@ import { Fragment, h, VNode } from "preact"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { State } from "./index.js"; import { format } from "date-fns"; -import { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, assertUnreachable } from "@gnu-taler/taler-util"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; -import { assertUnreachable } from "../Routing.js"; -import { Attention } from "../Attention.js"; +import { Attention } from "@gnu-taler/web-util/browser"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx @@ -1,48 +0,0 @@ -import { h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; - - - -export function CopyIcon(): VNode { - return ( - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> - </svg> - ) -}; - -export function CopiedIcon(): VNode { - return ( - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> - </svg> - ) -}; - -export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode { - const [copied, setCopied] = useState(false); - function copyText(): void { - navigator.clipboard.writeText(getContent() || ""); - setCopied(true); - } - useEffect(() => { - if (copied) { - setTimeout(() => { - setCopied(false); - }, 1000); - } - }, [copied]); - - if (!copied) { - return ( - <button class={clazz} onClick={copyText} > - <CopyIcon /> - </button> - ); - } - return ( - <button class={clazz} disabled> - <CopiedIcon /> - </button> - ); -} diff --git a/packages/demobank-ui/src/components/EmptyComponentExample/index.ts b/packages/demobank-ui/src/components/EmptyComponentExample/index.ts @@ -14,10 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Loading } from "../../components/Loading.js"; -import { HookError, utils } from "@gnu-taler/web-util/browser"; -//import { compose, StateViewMap } from "../../utils/index.js"; -//import { wxApi } from "../../wxApi.js"; +import { HookError, Loading, utils } from "@gnu-taler/web-util/browser"; import { useComponentState } from "./state.js"; import { LoadingUriView, ReadyView } from "./views.js"; diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx @@ -1,120 +0,0 @@ -/* -/* - 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 { TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { Attention } from "./Attention.js"; -import { assertUnreachable } from "./Routing.js"; - -export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { - const { i18n } = useTranslationContext() - switch (error.errorDetail.code) { - ////////////////// - // Every error that can be produce in a Http Request - ////////////////// - case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { - if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { - const { requestMethod, requestUrl, timeoutMs } = error.errorDetail - return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)} - </pre> - } - </Attention> - } - assertUnreachable(1 as never) - } - case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { - if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { - const { requestMethod, requestUrl, throttleStats } = error.errorDetail - return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)} - </pre> - } - </Attention> - } - assertUnreachable(1 as never) - } - case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { - if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { - const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail - return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)} - </pre> - } - </Attention> - } - assertUnreachable(1 as never) - } - case TalerErrorCode.WALLET_NETWORK_ERROR: { - if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { - const { requestMethod, requestUrl } = error.errorDetail - return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)} - </pre> - } - </Attention> - } - assertUnreachable(1 as never) - } - case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { - if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { - const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail - return <Attention type="danger" title={i18n.str`Unexpected request error`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)} - </pre> - } - </Attention> - } - assertUnreachable(1 as never) - } - ////////////////// - // Every other error - ////////////////// - // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { - // return <Attention type="danger" title={i18n.str``}> - // </Attention> - // } - ////////////////// - // Default message for unhandled case - ////////////////// - default: return <Attention type="danger" title={i18n.str`Unexpected error`}> - {error.message} - {showDetail && - <pre class="whitespace-break-spaces "> - {JSON.stringify(error.errorDetail, undefined, 2)} - </pre> - } - </Attention> - } -} - diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx @@ -1,111 +0,0 @@ -/* - 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { strings as messages } from "../i18n/strings.js"; -import langIcon from "../assets/lang.svg"; - -type LangsNames = { - [P in keyof typeof messages]: string; -}; - -const names: LangsNames = { - es: "Español [es]", - en: "English [en]", - fr: "Français [fr]", - de: "Deutsch [de]", - sv: "Svenska [sv]", - it: "Italiano [it]", -}; - -function getLangName(s: keyof LangsNames | string): string { - if (names[s]) return names[s]; - return String(s); -} - -export function LangSelector(): VNode { - const [updatingLang, setUpdatingLang] = useState(false); - const { lang, changeLanguage } = useTranslationContext(); - const [hidden, setHidden] = useState(true); - - useEffect(() => { - function bodyKeyPress(event: KeyboardEvent) { - if (event.code === "Escape") setHidden(true); - } - function bodyOnClick(event: Event) { - setHidden(true); - } - document.body.addEventListener("click", bodyOnClick); - document.body.addEventListener("keydown", bodyKeyPress as any); - return () => { - document.body.removeEventListener("keydown", bodyKeyPress as any); - document.body.removeEventListener("click", bodyOnClick); - }; - }, []); - return ( - <div> - <div class="relative mt-2"> - <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" - onClick={() => { - setHidden((h) => !h); - }}> - <span class="flex items-center"> - <img src={langIcon} alt="" class="h-5 w-5 flex-shrink-0 rounded-full" /> - <span class="ml-3 block truncate">{getLangName(lang)}</span> - </span> - <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> - <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" /> - </svg> - </span> - </button> - - {!hidden && - <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> - {Object.keys(messages) - .filter((l) => l !== lang) - .map((lang) => ( - <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option" - onClick={() => { - changeLanguage(lang); - setUpdatingLang(false); - setHidden(true) - }} - > - <span class="font-normal block truncate">{getLangName(lang)}</span> - - <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4"> - {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" /> - </svg> */} - </span> - </li> - ))} - - </ul> - } - - </div> - </div> - ); -} diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx @@ -323,7 +323,3 @@ function Redirect({ to }: { to: string }): VNode { }, []); return <div>being redirected to {to}</div>; } - -export function assertUnreachable(x: never): never { - throw new Error("Didn't expect to get here"); -} diff --git a/packages/demobank-ui/src/components/ShowLocalNotification.tsx b/packages/demobank-ui/src/components/ShowLocalNotification.tsx @@ -1,43 +0,0 @@ -import { Notification } from "@gnu-taler/web-util/browser"; -import { h, Fragment, VNode } from "preact"; -import { Attention } from "./Attention.js"; -import { useSettings } from "../hooks/settings.js"; - -export function ShowLocalNotification({ notification }: { notification?: Notification }): VNode { - if (!notification) return <Fragment /> - switch (notification.message.type) { - case "error": - return <div class="relative"> - <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> - <Attention type="danger" title={notification.message.title} onClose={() => { - notification.remove() - }}> - {notification.message.description && - <div class="mt-2 text-sm text-red-700"> - {notification.message.description} - </div> - } - <MaybeShowDebugInfo info={notification.message.debug} /> - </Attention> - </div> - </div> - case "info": - return <div class="relative"> - <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> - <Attention type="success" title={notification.message.title} onClose={() => { - notification.remove(); - }} /></div></div> - } -} - - -function MaybeShowDebugInfo({ info }: { info: any }): VNode { - const [settings] = useSettings() - if (settings.showDebugInfo) { - return <pre class="whitespace-break-spaces "> - {info} - </pre> - } - return <Fragment /> -} - diff --git a/packages/demobank-ui/src/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts @@ -14,10 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpError, utils } from "@gnu-taler/web-util/browser"; -import { Loading } from "../Loading.js"; -// import { compose, StateViewMap } from "../../utils/index.js"; -// import { wxApi } from "../../wxApi.js"; +import { Loading, utils } from "@gnu-taler/web-util/browser"; import { AbsoluteTime, AmountJson, TalerError } from "@gnu-taler/taler-util"; import { useComponentState } from "./state.js"; import { LoadingUriView, ReadyView } from "./views.js"; diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx @@ -72,6 +72,7 @@ function getInitialBackendBaseURL(): string { ? localStorage.getItem("bank-base-url") : undefined; let result: string; + if (!overrideUrl) { //normal path if (!bankUiSettings.backendBaseURL) { diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts @@ -18,7 +18,7 @@ import { TalerCorebankApi, TalerCoreBankHttpClient, TalerError } from "@gnu-tale import { BrowserHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact"; import { useContext, useEffect, useState } from "preact/hooks"; -import { ErrorLoading } from "../components/ErrorLoading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; /** * diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -16,8 +16,8 @@ import { AbsoluteTime, AmountJson, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; import { utils } from "@gnu-taler/web-util/browser"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { LoginForm } from "../LoginForm.js"; import { useComponentState } from "./state.js"; import { InvalidIbanView, ReadyView } from "./views.js"; diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -16,7 +16,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Attention } from "../../components/Attention.js"; +import { Attention } from "@gnu-taler/web-util/browser"; import { Transactions } from "../../components/Transactions/index.js"; import { useSettings } from "../../hooks/settings.js"; import { PaymentOptions } from "../PaymentOptions.js"; diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -14,15 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, TalerError, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { Attention, LangSelector, Loading, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import logo from "../assets/logo-2021.svg"; -import { Attention } from "../components/Attention.js"; -import { CopyButton } from "../components/CopyButton.js"; -import { LangSelector } from "../components/LangSelector.js"; -import { Loading } from "../components/Loading.js"; import { useAccountDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; import { getAllBooleanSettings, getLabelForSetting, useSettings } from "../hooks/settings.js"; @@ -179,7 +175,7 @@ export function BankFrame({ </li> : undefined} <li> - <LangSelector /> + <LangSelector supportedLangs={["en", "es", "de"]} /> </li> <li> <div class="text-xs font-semibold leading-6 text-gray-400"> diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -18,15 +18,15 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { Notification, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; -import { Attention } from "../components/Attention.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { Attention } from "@gnu-taler/web-util/browser"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; /** diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -16,8 +16,8 @@ import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerError, TalerErrorDetail, TranslatedString, WithdrawUriResult } from "@gnu-taler/taler-util"; import { utils } from "@gnu-taler/web-util/browser"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { useComponentState } from "./state.js"; import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js"; diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -19,13 +19,13 @@ import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-ta import { Fragment, VNode, h } from "preact"; import { useEffect, useMemo, useState } from "preact/hooks"; import { QR } from "../../components/QR.js"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useSettings } from "../../hooks/settings.js"; import { undefinedIfEmpty } from "../../utils.js"; import { State } from "./index.js"; -import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Attention } from "../../components/Attention.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Attention } from "@gnu-taler/web-util/browser"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -32,7 +32,7 @@ import { import { Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { @@ -41,7 +41,7 @@ import { withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; const logger = new Logger("PaytoWireTransferForm"); diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx @@ -18,7 +18,7 @@ import { Logger, TalerError } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { Loading } from "../components/Loading.js"; +import { Loading } from "@gnu-taler/web-util/browser"; import { Transactions } from "../components/Transactions/index.js"; import { usePublicAccounts } from "../hooks/access.js"; diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -29,7 +29,7 @@ import { QR } from "../components/QR.js"; import { useBankCoreApiContext } from "../context/config.js"; import { withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; export function QrCodeSection({ withdrawUri, diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -20,13 +20,13 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { getRandomPassword, getRandomUsername } from "./rnd.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; const logger = new Logger("RegistrationPage"); diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -2,8 +2,8 @@ import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { ErrorLoading } from "../components/ErrorLoading.js"; -import { Loading } from "../components/Loading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useAccountDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; @@ -12,7 +12,7 @@ import { LoginForm } from "./LoginForm.js"; import { ProfileNavigation } from "./ProfileNavigation.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { AccountForm } from "./admin/AccountForm.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; export function ShowAccountDetails({ account, diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -1,14 +1,14 @@ import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { ProfileNavigation } from "./ProfileNavigation.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; export function UpdateAccountPassword({ account: accountName, @@ -63,7 +63,7 @@ export function UpdateAccountPassword({ }) case "old-password-invalid-or-not-allowed": return notify({ type: "error", - title: current ? + title: current ? i18n.str`This user have no right on to change the password.` : i18n.str`This user have no right on to change the password or the old password doesn't match.` }) @@ -79,7 +79,7 @@ export function UpdateAccountPassword({ return ( <Fragment> - <ShowLocalNotification notification={notification} /> + <ShowLocalNotification notification={notification} /> {accountIsTheCurrentUser ? <ProfileNavigation current="credentials" /> : <h1 class="text-base font-semibold leading-6 text-gray-900"> diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -29,7 +29,7 @@ import { import { Fragment, VNode, h } from "preact"; import { forwardRef } from "preact/compat"; import { useState } from "preact/hooks"; -import { Attention } from "../components/Attention.js"; +import { Attention } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { useSettings } from "../hooks/settings.js"; @@ -37,7 +37,7 @@ import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { OperationState } from "./OperationState/index.js"; import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(InputAmount); diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -31,13 +31,13 @@ import { import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; import { mutate } from "swr"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useSettings } from "../hooks/settings.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; const logger = new Logger("WithdrawalConfirmationQuestion"); diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx @@ -23,7 +23,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Attention } from "../components/Attention.js"; +import { Attention } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useSettings } from "../hooks/settings.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -23,9 +23,9 @@ import { } from "@gnu-taler/taler-util"; import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Attention } from "../components/Attention.js"; -import { ErrorLoading } from "../components/ErrorLoading.js"; -import { Loading } from "../components/Loading.js"; +import { Attention } from "@gnu-taler/web-util/browser"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { useWithdrawalDetails } from "../hooks/access.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -1,8 +1,8 @@ import { Amounts, TalerError } from "@gnu-taler/taler-util"; import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { useAccountDetails } from "../../hooks/access.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { LoginForm } from "../LoginForm.js"; diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,11 +1,11 @@ import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { useEffect, useRef, useState } from "preact/hooks"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; -import { CopyButton } from "../../components/CopyButton.js"; +import { CopyButton } from "@gnu-taler/web-util/browser"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -1,8 +1,8 @@ import { Amounts, TalerError } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBusinessAccounts } from "../../hooks/circuit.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx @@ -2,7 +2,7 @@ import { AmountString, Amounts, TalerCorebankApi, TalerError } from "@gnu-taler/ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; import { Transactions } from "../../components/Transactions/index.js"; import { useLastMonitorInfo } from "../../hooks/circuit.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -3,14 +3,14 @@ import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-ta import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { Attention } from "../../components/Attention.js"; +import { Attention } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { withRuntimeErrorHandling } from "../../utils.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { getRandomPassword } from "../rnd.js"; import { AccountForm, AccountFormData } from "./AccountForm.js"; -import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; export function CreateNewAccount({ onCancel, diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -2,10 +2,10 @@ import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util"; import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { Attention } from "../../components/Attention.js"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { Attention } from "@gnu-taler/web-util/browser"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; @@ -13,7 +13,7 @@ import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; export function RemoveAccount({ account, diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -25,10 +25,10 @@ import { import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { mutate } from "swr"; -import { Attention } from "../../components/Attention.js"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { Attention } from "@gnu-taler/web-util/browser"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; @@ -43,7 +43,7 @@ import { import { LoginForm } from "../LoginForm.js"; import { InputAmount } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; interface Props { account: string; diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -25,10 +25,10 @@ import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { Attention } from "../../components/Attention.js"; -import { ErrorLoading } from "../../components/ErrorLoading.js"; -import { Loading } from "../../components/Loading.js"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { Attention } from "@gnu-taler/web-util/browser"; +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { @@ -39,7 +39,7 @@ import { withRuntimeErrorHandling } from "../../utils.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; +import { ShowLocalNotification } from "@gnu-taler/web-util/browser"; interface Props { id: string; diff --git a/packages/demobank-ui/tailwind.config.js b/packages/demobank-ui/tailwind.config.js @@ -1,6 +1,12 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./src/**/*.{html,tsx}"], + content: { + relative: true, + files: [ + "./src/**/*.{html,tsx}", + "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}" + ], + }, theme: { extend: {}, }, diff --git a/packages/demobank-ui/src/assets/lang.svg b/packages/web-util/src/assets/lang.svg diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx @@ -0,0 +1,58 @@ +import { TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; + +interface Props { + type?: "info" | "success" | "warning" | "danger", + onClose?: () => void, + title: TranslatedString, + children?: ComponentChildren, +} +export function Attention({ type = "info", title, children, onClose }: Props): VNode { + return <div class={`group attention-${type} mt-2 shadow-lg`}> + <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> + <div class="flex"> + <div > + <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400"> + {(() => { + switch (type) { + case "info": + return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" /> + case "warning": + return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "danger": + return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "success": + return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /> + default: + assertUnreachable(type) + } + })()} + </svg> + </div> + <div class="ml-3 w-full"> + <h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800"> + {title} + </h3> + <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700"> + {children} + </div> + </div> + {onClose && + <div> + <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" + onClick={(e) => { + e.preventDefault(); + onClose(); + }} + > + <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> + </svg> + </button> + </div> + } + </div> + </div> + + </div> +} diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx @@ -0,0 +1,46 @@ +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; + +export function CopyIcon(): VNode { + return ( + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> + </svg> + ) +}; + +export function CopiedIcon(): VNode { + return ( + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> + </svg> + ) +}; + +export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode { + const [copied, setCopied] = useState(false); + function copyText(): void { + navigator.clipboard.writeText(getContent() || ""); + setCopied(true); + } + useEffect(() => { + if (copied) { + setTimeout(() => { + setCopied(false); + }, 1000); + } + }, [copied]); + + if (!copied) { + return ( + <button class={clazz} onClick={copyText} > + <CopyIcon /> + </button> + ); + } + return ( + <button class={clazz} disabled> + <CopiedIcon /> + </button> + ); +} diff --git a/packages/web-util/src/components/ErrorLoading.tsx b/packages/web-util/src/components/ErrorLoading.tsx @@ -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 { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { Attention } from "./Attention.js"; +import { useTranslationContext } from "../index.browser.js"; + +export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { + const { i18n } = useTranslationContext() + switch (error.errorDetail.code) { + ////////////////// + // Every error that can be produce in a Http Request + ////////////////// + case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + const { requestMethod, requestUrl, throttleStats } = error.errorDetail + return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { + if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { + const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail + return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_NETWORK_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { + const { requestMethod, requestUrl } = error.errorDetail + return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { + const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail + return <Attention type="danger" title={i18n.str`Unexpected request error`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + ////////////////// + // Every other error + ////////////////// + // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + // return <Attention type="danger" title={i18n.str``}> + // </Attention> + // } + ////////////////// + // Default message for unhandled case + ////////////////// + default: return <Attention type="danger" title={i18n.str`Unexpected error`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify(error.errorDetail, undefined, 2)} + </pre> + } + </Attention> + } +} + diff --git a/packages/web-util/src/components/LangSelector.tsx b/packages/web-util/src/components/LangSelector.tsx @@ -0,0 +1,111 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +// import { strings as messages } from "../i18n/strings.js"; +import langIcon from "../assets/lang.svg"; +import { useTranslationContext } from "../index.browser.js"; + +type LangsNames = { + [P: string]: string; +}; + +const names: LangsNames = { + es: "Español [es]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", +}; + +function getLangName(s: keyof LangsNames | string): string { + if (names[s]) return names[s]; + return String(s); +} + +export function LangSelector({ supportedLangs }: { supportedLangs: string[] }): VNode { + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); + const [hidden, setHidden] = useState(true); + + useEffect(() => { + function bodyKeyPress(event: KeyboardEvent) { + if (event.code === "Escape") setHidden(true); + } + function bodyOnClick(event: Event) { + setHidden(true); + } + document.body.addEventListener("click", bodyOnClick); + document.body.addEventListener("keydown", bodyKeyPress as any); + return () => { + document.body.removeEventListener("keydown", bodyKeyPress as any); + document.body.removeEventListener("click", bodyOnClick); + }; + }, []); + return ( + <div> + <div class="relative mt-2"> + <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" + onClick={() => { + setHidden((h) => !h); + }}> + <span class="flex items-center"> + <img alt="language" class="h-5 w-5 flex-shrink-0 rounded-full" src={langIcon} /> + <span class="ml-3 block truncate">{getLangName(lang)}</span> + </span> + <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> + <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" /> + </svg> + </span> + </button> + + {!hidden && + <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> + {supportedLangs + .filter((l) => l !== lang) + .map((lang) => ( + <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option" + onClick={() => { + changeLanguage(lang); + setUpdatingLang(false); + setHidden(true) + }} + > + <span class="font-normal block truncate">{getLangName(lang)}</span> + + <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4"> + {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" /> + </svg> */} + </span> + </li> + ))} + + </ul> + } + + </div> + </div> + ); +} diff --git a/packages/demobank-ui/src/components/Loading.tsx b/packages/web-util/src/components/Loading.tsx diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx b/packages/web-util/src/components/ShowInputErrorLabel.tsx diff --git a/packages/web-util/src/components/ShowLocalNotification.tsx b/packages/web-util/src/components/ShowLocalNotification.tsx @@ -0,0 +1,43 @@ +import { h, Fragment, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { Notification } from "../index.browser.js"; +// import { useSettings } from "../hooks/settings.js"; + +export function ShowLocalNotification({ notification }: { notification?: Notification }): VNode { + if (!notification) return <Fragment /> + switch (notification.message.type) { + case "error": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="danger" title={notification.message.title} onClose={() => { + notification.remove() + }}> + {notification.message.description && + <div class="mt-2 text-sm text-red-700"> + {notification.message.description} + </div> + } + {/* <MaybeShowDebugInfo info={notification.message.debug} /> */} + </Attention> + </div> + </div> + case "info": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="success" title={notification.message.title} onClose={() => { + notification.remove(); + }} /></div></div> + } +} + + +// function MaybeShowDebugInfo({ info }: { info: any }): VNode { +// const [settings] = useSettings() +// if (settings.showDebugInfo) { +// return <pre class="whitespace-break-spaces "> +// {info} +// </pre> +// } +// return <Fragment /> +// } + diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts @@ -1 +1,8 @@ export * as utils from "./utils.js"; +export * from "./Attention.js"; +export * from "./CopyButton.js"; +export * from "./ErrorLoading.js"; +export * from "./LangSelector.js"; +export * from "./Loading.js"; +export * from "./ShowInputErrorLabel.js"; +export * from "./ShowLocalNotification.js"; diff --git a/packages/web-util/src/declaration.d.ts b/packages/web-util/src/declaration.d.ts @@ -0,0 +1,35 @@ +/* + 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/> + */ + +declare module "*.css" { + const mapping: Record<string, string>; + export default mapping; +} +declare module "*.svg" { + const content: any; + export default content; +} +declare module "*.jpeg" { + const content: any; + export default content; +} +declare module "*.png" { + const content: any; + export default content; +} + +declare const __VERSION__: string; +declare const __GIT_HASH__: string;