From 236d4347f5884bb1d9ca1d3bb4ad0ba776577fd2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 23 Jan 2024 18:00:42 -0300 Subject: many changes activate eslint update file headers removed history and preact-router remove eslint errors and more applied prettier --- packages/demobank-ui/.eslintrc.cjs | 28 + packages/demobank-ui/build.mjs | 2 +- packages/demobank-ui/copyleft-header.js | 2 +- packages/demobank-ui/dev.mjs | 2 +- packages/demobank-ui/package.json | 32 +- packages/demobank-ui/src/Routing.tsx | 770 ++- .../demobank-ui/src/components/Cashouts/index.ts | 21 +- .../demobank-ui/src/components/Cashouts/state.ts | 11 +- .../demobank-ui/src/components/Cashouts/test.ts | 5 +- .../demobank-ui/src/components/Cashouts/views.tsx | 201 +- .../src/components/EmptyComponentExample/state.ts | 2 +- .../src/components/EmptyComponentExample/views.tsx | 15 +- .../src/components/ErrorLoadingWithDebug.tsx | 17 +- .../src/components/Transactions/state.ts | 60 +- .../src/components/Transactions/test.ts | 13 +- .../src/components/Transactions/views.tsx | 218 +- packages/demobank-ui/src/components/app.tsx | 56 +- packages/demobank-ui/src/context/backend.ts | 8 +- packages/demobank-ui/src/context/config.ts | 175 +- packages/demobank-ui/src/context/settings.ts | 2 +- packages/demobank-ui/src/declaration.d.ts | 6 +- packages/demobank-ui/src/forms/simplest.ts | 66 - packages/demobank-ui/src/hooks/access.ts | 167 +- packages/demobank-ui/src/hooks/async.ts | 15 +- packages/demobank-ui/src/hooks/backend.ts | 20 +- packages/demobank-ui/src/hooks/bank-state.ts | 99 +- packages/demobank-ui/src/hooks/circuit.ts | 310 +- packages/demobank-ui/src/hooks/index.ts | 2 +- packages/demobank-ui/src/hooks/preferences.ts | 50 +- packages/demobank-ui/src/i18n/strings.ts | 7195 ++++++-------------- packages/demobank-ui/src/index.html | 44 +- packages/demobank-ui/src/index.tsx | 8 +- packages/demobank-ui/src/pages.ts | 44 - .../demobank-ui/src/pages/AccountPage/index.ts | 42 +- .../demobank-ui/src/pages/AccountPage/state.ts | 58 +- packages/demobank-ui/src/pages/AccountPage/test.ts | 15 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 127 +- packages/demobank-ui/src/pages/BankFrame.tsx | 244 +- packages/demobank-ui/src/pages/DownloadStats.tsx | 494 +- packages/demobank-ui/src/pages/LoginForm.tsx | 213 +- .../demobank-ui/src/pages/OperationState/index.ts | 99 +- .../demobank-ui/src/pages/OperationState/state.ts | 143 +- .../demobank-ui/src/pages/OperationState/test.ts | 15 +- .../demobank-ui/src/pages/OperationState/views.tsx | 497 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 184 +- .../src/pages/PaytoWireTransferForm.tsx | 635 +- .../demobank-ui/src/pages/ProfileNavigation.tsx | 214 +- .../demobank-ui/src/pages/PublicHistoriesPage.tsx | 32 +- packages/demobank-ui/src/pages/QrCodeSection.tsx | 121 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 257 +- .../demobank-ui/src/pages/SolveChallengePage.tsx | 806 ++- .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 367 +- packages/demobank-ui/src/pages/WireTransfer.tsx | 60 +- .../src/pages/WithdrawalConfirmationQuestion.tsx | 325 +- .../src/pages/WithdrawalOperationPage.tsx | 47 +- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 298 +- .../src/pages/account/CashoutListForAccount.tsx | 76 +- .../src/pages/account/ShowAccountDetails.tsx | 220 +- .../src/pages/account/UpdateAccountPassword.tsx | 173 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 807 ++- .../demobank-ui/src/pages/admin/AccountList.tsx | 288 +- packages/demobank-ui/src/pages/admin/AdminHome.tsx | 650 +- .../src/pages/admin/CreateNewAccount.tsx | 230 +- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 194 +- .../src/pages/business/CreateCashout.tsx | 544 +- .../src/pages/business/ShowCashoutDetails.tsx | 136 +- packages/demobank-ui/src/pages/rnd.ts | 36 +- packages/demobank-ui/src/route.ts | 335 +- packages/demobank-ui/src/settings.json | 2 +- packages/demobank-ui/src/settings.ts | 60 +- packages/demobank-ui/src/stories.test.ts | 9 +- packages/demobank-ui/src/utils.ts | 85 +- packages/demobank-ui/test.mjs | 6 +- 73 files changed, 9129 insertions(+), 9681 deletions(-) create mode 100644 packages/demobank-ui/.eslintrc.cjs delete mode 100644 packages/demobank-ui/src/forms/simplest.ts delete mode 100644 packages/demobank-ui/src/pages.ts (limited to 'packages/demobank-ui') diff --git a/packages/demobank-ui/.eslintrc.cjs b/packages/demobank-ui/.eslintrc.cjs new file mode 100644 index 000000000..05618b499 --- /dev/null +++ b/packages/demobank-ui/.eslintrc.cjs @@ -0,0 +1,28 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'header'], + root: true, + rules: { + "react/no-unknown-property": 0, + "react/no-unescaped-entities": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/no-unused-vars": [2,{argsIgnorePattern:"^_"}], + "header/header": [2,"copyleft-header.js"] + }, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + jsx: true, + }, + settings: { + react: { + version: "18", + pragma: "h", + } + }, +}; diff --git a/packages/demobank-ui/build.mjs b/packages/demobank-ui/build.mjs index 64ddc3774..04a6f646b 100755 --- a/packages/demobank-ui/build.mjs +++ b/packages/demobank-ui/build.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/packages/demobank-ui/copyleft-header.js b/packages/demobank-ui/copyleft-header.js index 2635717c5..7fa276bea 100644 --- a/packages/demobank-ui/copyleft-header.js +++ b/packages/demobank-ui/copyleft-header.js @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs index c5ea318e7..7b4f719ae 100755 --- a/packages/demobank-ui/dev.mjs +++ b/packages/demobank-ui/dev.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json index 22c4dc874..14fb28081 100644 --- a/packages/demobank-ui/package.json +++ b/packages/demobank-ui/package.json @@ -20,29 +20,13 @@ "@gnu-taler/taler-util": "workspace:*", "@gnu-taler/web-util": "workspace:*", "date-fns": "2.29.3", - "history": "4.10.1", "jed": "1.1.1", "preact": "10.11.3", - "preact-router": "3.2.1", "qrcode-generator": "^1.4.4", "swr": "2.0.3" }, - "eslintConfig": { - "plugins": [ - "header" - ], - "rules": { - "header/header": [ - 2, - "copyleft-header.js" - ] - }, - "extends": [ - "prettier" - ] - }, "devDependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", + "eslint": "^8.56.0", "@gnu-taler/pogen": "^0.0.5", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", @@ -50,19 +34,15 @@ "@types/history": "^4.7.8", "@types/mocha": "^10.0.1", "@types/node": "^18.11.17", - "@typescript-eslint/eslint-plugin": "^5.41.0", - "@typescript-eslint/parser": "^5.41.0", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", "autoprefixer": "^10.4.14", - "bulma": "^0.9.4", - "bulma-checkbox": "^1.1.1", - "bulma-radio": "^1.1.1", "chai": "^4.3.6", "esbuild": "^0.19.9", - "eslint-config-preact": "^1.2.0", - "mocha": "^9.2.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react": "^7.33.2", + "mocha": "9.2.0", "po2json": "^0.4.5", - "preact-render-to-string": "^5.2.6", - "sass": "1.56.1", "tailwindcss": "^3.3.2", "typescript": "5.3.3" }, diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx index c66d9de0c..e73493d60 100644 --- a/packages/demobank-ui/src/Routing.tsx +++ b/packages/demobank-ui/src/Routing.tsx @@ -14,386 +14,470 @@ GNU Taler; see the file COPYING. If not, see */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { createHashHistory } from "history"; +import { + LocalNotificationBanner, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Route, Router, route } from "preact-router"; -import { useEffect } from "preact/hooks"; +import { + AccessToken, + HttpStatusCode, + TranslatedString, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { useBankCoreApiContext } from "./context/config.js"; +import { useSettingsContext } from "./context/settings.js"; import { useBackendState } from "./hooks/backend.js"; +import { AccountPage } from "./pages/AccountPage/index.js"; import { BankFrame } from "./pages/BankFrame.js"; -import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js"; +import { DownloadStats } from "./pages/DownloadStats.js"; import { LoginForm } from "./pages/LoginForm.js"; import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js"; import { RegistrationPage } from "./pages/RegistrationPage.js"; -import { AdminHome } from "./pages/admin/AdminHome.js"; -import { CreateCashout } from "./pages/business/CreateCashout.js"; +import { SolveChallengePage } from "./pages/SolveChallengePage.js"; +import { WireTransfer } from "./pages/WireTransfer.js"; +import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js"; +import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js"; import { ShowAccountDetails } from "./pages/account/ShowAccountDetails.js"; import { UpdateAccountPassword } from "./pages/account/UpdateAccountPassword.js"; -import { RemoveAccount } from "./pages/admin/RemoveAccount.js"; +import { AdminHome } from "./pages/admin/AdminHome.js"; import { CreateNewAccount } from "./pages/admin/CreateNewAccount.js"; -import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js"; +import { RemoveAccount } from "./pages/admin/RemoveAccount.js"; +import { CreateCashout } from "./pages/business/CreateCashout.js"; import { ShowCashoutDetails } from "./pages/business/ShowCashoutDetails.js"; -import { WireTransfer } from "./pages/WireTransfer.js"; -import { AccountPage } from "./pages/AccountPage/index.js"; -import { useSettingsContext } from "./context/settings.js"; -import { useBankCoreApiContext } from "./context/config.js"; -import { DownloadStats } from "./pages/DownloadStats.js"; -import { SolveChallengePage } from "./pages/SolveChallengePage.js"; +import { RouteParamsType, urlPattern, useCurrentLocation } from "./route.js"; export function Routing(): VNode { - const history = createHashHistory(); const backend = useBackendState(); + + if (backend.state.status === "loggedIn") { + const { isUserAdministrator, username } = backend.state; + return ( + + + + ); + } + return ( + + { + backend.logIn({ username, token: token }); + }} + /> + + ); +} + +const publicPages = { + login: urlPattern(/\/login/, () => "#/login"), + register: urlPattern(/\/register/, () => "#/register"), + publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"), + operationDetails: urlPattern<{ wopid: string }>( + /\/operation\/(?[a-zA-Z0-9]+)/, + ({ wopid }) => `#/operation/${wopid}`, + ), + solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"), +}; + +function PublicRounting({ + onLoggedUser, +}: { + onLoggedUser: (username: string, token: AccessToken) => void; +}): VNode { const settings = useSettingsContext(); - const { config } = useBankCoreApiContext(); const { i18n } = useTranslationContext(); + const [loc, routeTo] = useCurrentLocation(publicPages); + const { api } = useBankCoreApiContext(); + const [notification, notify, handleError] = useLocalNotification(); - if (backend.state.status === "loggedOut") { - return - - ( - -
-

{i18n.str`Welcome to ${settings.bankName}!`}

-
+ if (loc === undefined) { + routeTo("login", {}); + return ; + } - { - route("/register"); - }} - /> - - )} - /> - } - /> - ( - { - route(`/2fa`) - }} - onContinue={() => { - route("/account"); - }} - /> - )} + async function doAutomaticLogin(username: string, password: string) { + await handleError(async () => { + const resp = await api + .getAuthenticationAPI(username) + .createAccessToken(password, { + scope: "readwrite", + duration: { d_us: "forever" }, + refreshable: true, + }); + if (resp.type === "ok") { + onLoggedUser(username, resp.body.access_token); + } else { + switch (resp.case) { + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${username}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: + assertUnreachable(resp); + } + } + }); + } + + switch (loc.name) { + case "login": { + return ( + +
+

{i18n.str`Welcome to ${settings.bankName}!`}

+
+ + +
+ ); + } + case "publicAccounts": { + return ; + } + case "operationDetails": { + const { wopid } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + + return ( + routeTo("login", {})} + routeClose={publicPages.login} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} /> - {config.allow_registrations && - ( - { - route("/account"); - }} - onCancel={() => { - route("/account"); - }} - /> - )} + ); + } + case "register": { + return ( + + + - } - -
-
+ + ); + } + case "solveSecondFactor": { + return ( + routeTo("login", {})} + routeClose={publicPages.login} + /> + ); + } + default: + assertUnreachable(loc.name); } - const { isUserAdministrator, username } = backend.state +} - return ( - - - ( - { - route("/account"); - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - /> - )} +export const privatePages = { + home: urlPattern(/\/account/, () => "#/account"), + homeChargeWallet: urlPattern( + /\/account\/charge-wallet/, + () => "#/account/charge-wallet", + ), + homeWireTransfer: urlPattern( + /\/account\/wire-transfer/, + () => "#/account/wire-transfer", + ), + solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"), + cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"), + cashoutDetails: urlPattern<{ cid: string }>( + /\/cashout\/(?[a-zA-Z0-9]+)/, + ({ cid }) => `#/cashout/${cid}`, + ), + wireTranserCreate: urlPattern<{ destination: string }>( + /\/wire-transfer\/(?[a-zA-Z0-9]+)/, + ({ destination }) => `#/wire-transfer/${destination}`, + ), + publicAccountList: urlPattern(/\/public-accounts/, () => "#/public-accounts"), + statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"), + accountCreate: urlPattern(/\/new-account/, () => "#/new-account"), + myAccountDelete: urlPattern( + /\/delete-my-account/, + () => "#/delete-my-account", + ), + myAccountDetails: urlPattern(/\/my-profile/, () => "#/my-profile"), + myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"), + myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"), + accountDetails: urlPattern<{ account: string }>( + /\/profile\/(?[a-zA-Z0-9]+)\/details/, + ({ account }) => `#/profile/${account}/details`, + ), + accountChangePassword: urlPattern<{ account: string }>( + /\/profile\/(?[a-zA-Z0-9]+)\/change-password/, + ({ account }) => `#/profile/${account}/change-password`, + ), + accountDelete: urlPattern<{ account: string }>( + /\/profile\/(?[a-zA-Z0-9]+)\/delete/, + ({ account }) => `#/profile/${account}/delete`, + ), + accountCashouts: urlPattern<{ account: string }>( + /\/profile\/(?[a-zA-Z0-9]+)\/cashouts/, + ({ account }) => `#/profile/${account}/cashouts`, + ), + operationDetails: urlPattern<{ wopid: string }>( + /\/operation\/(?[a-zA-Z0-9]+)/, + ({ wopid }) => `#/operation/${wopid}`, + ), +}; + +function PrivateRouting({ + username, + isAdmin, +}: { + username: string; + isAdmin: boolean; +}): VNode { + const [loc, routeTo] = useCurrentLocation(privatePages); + + if (loc === undefined) { + routeTo("home", {}); + return ; + } + + switch (loc.name) { + case "operationDetails": { + const { wopid } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + + return ( + routeTo("home", {})} + routeClose={privatePages.home} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} /> - ( - { - route("/account"); - }} - /> - )} + ); + } + case "solveSecondFactor": { + return ( + routeTo("home", {})} + routeClose={privatePages.home} /> - } + ); + } + case "publicAccountList": { + return ; + } + case "statsDownload": { + return ; + } + case "accountCreate": { + return ( + routeTo("home", {})} /> - { - route("/account") - }} - />} + ); + } + case "accountDetails": { + const { account } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - - { - route("/account") - }} - onCreateSuccess={() => { - route("/account") - }} - />} + ); + } + case "accountChangePassword": { + const { account } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - - ( - { - route("/account") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onClear={() => { - route("/account") - }} - /> - )} + ); + } + case "accountDelete": { + const { account } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeCancel={privatePages.home} /> - - ( - { - route("/account") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onCancel={() => { - route("/account") - }} - /> - )} + ); + } + case "accountCashouts": { + const { account } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + routeTo("solveSecondFactor", {})} /> - ( - { - route("/account") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onCancel={() => { - route("/account") - }} - /> - )} + ); + } + case "myAccountDelete": { + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeCancel={privatePages.home} /> - - ( - { - route(`/cashout/${cid}`) - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onClose={() => { - route("/account") - }} - /> - )} + ); + } + case "myAccountDetails": { + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - - ( - { - route("/") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onCancel={() => { - route("/account") - }} - /> - )} + ); + } + case "myAccountPassword": { + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - ( - { - route("/account") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onClear={() => { - route("/account") - }} - /> - )} + ); + } + case "myAccountCashouts": { + return ( + routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - ( - { - route("/account") - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onCancel={() => { - route("/account") - }} - /> - )} + ); + } + case "home": { + if (isAdmin) { + return ( + routeTo("solveSecondFactor", {})} + routeCreate={privatePages.accountCreate} + routeRemoveAccount={privatePages.accountDelete} + routeShowAccount={privatePages.accountDetails} + routeShowCashoutsAccount={privatePages.accountCashouts} + routeUpdatePasswordAccount={privatePages.accountChangePassword} + /> + ); + } + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })} /> - - ( - { - route(`/cashout/${cid}`) - }} - onAuthorizationRequired={() => { - route(`/2fa`) - }} - onClose={() => { - route("/account"); - }} - /> - )} + ); + } + case "cashoutCreate": { + return ( + routeTo("solveSecondFactor", {})} + routeClose={privatePages.home} /> - - ( - { - route(`/2fa`) - }} - onCancel={() => { - route("/account"); - }} - /> - )} + ); + } + case "cashoutDetails": { + const { cid } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + - - ( - { - route("/my-cashouts"); - }} - /> - )} + ); + } + case "wireTranserCreate": { + const { destination } = loc.values as RouteParamsType< + typeof loc.parent, + typeof loc.name + >; + return ( + routeTo("solveSecondFactor", {})} + routeCancel={privatePages.home} + onSuccess={() => routeTo("home", {})} /> - - - ( - { - route(`/2fa`) - }} - onCancel={() => { - route("/account") - }} - onSuccess={() => { - route("/account") - }} - /> - )} + ); + } + case "homeChargeWallet": { + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })} /> - - { - if (isUserAdministrator) { - return { - route(`/2fa`) - }} - onCreateAccount={() => { - route("/new-account") - }} - onShowAccountDetails={(aid) => { - route(`/profile/${aid}/details`) - }} - onRemoveAccount={(aid) => { - route(`/profile/${aid}/delete`) - }} - onShowCashoutForAccount={(aid) => { - route(`/profile/${aid}/cashouts`) - }} - onUpdateAccountPassword={(aid) => { - route(`/profile/${aid}/change-password`) - - }} - />; - } else { - return { - route(`/2fa`) - }} - goToConfirmOperation={(wopid) => { - route(`/operation/${wopid}`); - }} - /> - } - }} + ); + } + case "homeWireTransfer": { + return ( + routeTo("home", {})} + onAuthorizationRequired={() => routeTo("solveSecondFactor", {})} + onOperationCreated={(wopid) => routeTo("operationDetails", { wopid })} /> - - - - ); -} - -function Redirect({ to }: { to: string }): VNode { - useEffect(() => { - route(to, true); - }, []); - return
being redirected to {to}
; + ); + } + default: + assertUnreachable(loc.name); + } } diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts index 2ee9237f8..2c6bf681c 100644 --- a/packages/demobank-ui/src/components/Cashouts/index.ts +++ b/packages/demobank-ui/src/components/Cashouts/index.ts @@ -15,17 +15,28 @@ */ import { Loading, utils } from "@gnu-taler/web-util/browser"; -import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + AmountJson, + TalerCoreBankErrorsByMethod, + TalerCorebankApi, + TalerError, +} from "@gnu-taler/taler-util"; import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; import { useComponentState } from "./state.js"; import { FailedView, ReadyView } from "./views.js"; +import { RouteDefinition } from "../../route.js"; export interface Props { account: string; - onSelected: (id: number) => void; + routeCashoutDetails: RouteDefinition<{ cid: string }>; } -export type State = State.Loading | State.Failed | State.LoadingUriError | State.Ready; +export type State = + | State.Loading + | State.Failed + | State.LoadingUriError + | State.Ready; export namespace State { export interface Loading { @@ -50,7 +61,7 @@ export namespace State { status: "ready"; error: undefined; cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[]; - onSelected: (id: number) => void; + routeCashoutDetails: RouteDefinition<{ cid: string }>; } } @@ -65,7 +76,7 @@ export interface Transaction { const viewMapping: utils.StateViewMap = { loading: Loading, "loading-error": ErrorLoadingWithDebug, - "failed": FailedView, + failed: FailedView, ready: ReadyView, }; diff --git a/packages/demobank-ui/src/components/Cashouts/state.ts b/packages/demobank-ui/src/components/Cashouts/state.ts index 0be7221b6..344b93e14 100644 --- a/packages/demobank-ui/src/components/Cashouts/state.ts +++ b/packages/demobank-ui/src/components/Cashouts/state.ts @@ -18,7 +18,10 @@ import { TalerError } from "@gnu-taler/taler-util"; import { useCashouts } from "../../hooks/circuit.js"; import { Props, State } from "./index.js"; -export function useComponentState({ account, onSelected }: Props): State { +export function useComponentState({ + account, + routeCashoutDetails, +}: Props): State { const result = useCashouts(account); if (!result) { return { @@ -35,14 +38,14 @@ export function useComponentState({ account, onSelected }: Props): State { if (result.type === "fail") { return { status: "failed", - error: result - } + error: result, + }; } return { status: "ready", error: undefined, cashouts: result.body.cashouts, - onSelected, + routeCashoutDetails, }; } diff --git a/packages/demobank-ui/src/components/Cashouts/test.ts b/packages/demobank-ui/src/components/Cashouts/test.ts index 031794b96..569cbc6f0 100644 --- a/packages/demobank-ui/src/components/Cashouts/test.ts +++ b/packages/demobank-ui/src/components/Cashouts/test.ts @@ -25,6 +25,7 @@ import { expect } from "chai"; import { CASHOUT_API_EXAMPLE } from "../../endpoints.js"; import { Props } from "./index.js"; import { useComponentState } from "./state.js"; +import { buildNullRoutDefinition } from "../../route.js"; describe("Cashout states", () => { it.skip("should query backend and render transactions", async () => { @@ -32,9 +33,7 @@ describe("Cashout states", () => { const props: Props = { account: "123", - onSelected: () => { - null; - }, + routeCashoutDetails: buildNullRoutDefinition(), }; env.addRequestExpectation(CASHOUT_API_EXAMPLE.LIST_FIRST_PAGE, { diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index d01187835..d036ec7d2 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -14,8 +14,17 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; -import { Attention, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + Amounts, + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Attention, + Loading, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useConversionInfo } from "../../hooks/circuit.js"; @@ -26,87 +35,132 @@ import { State } from "./index.js"; export function FailedView({ error }: State.Failed) { const { i18n } = useTranslationContext(); switch (error.case) { - case HttpStatusCode.NotImplemented: return -
- {error.detail.hint} -
-
case HttpStatusCode.NotImplemented: { - return - ; + return ( + + ); } - default: assertUnreachable(error.case) + default: + assertUnreachable(error.case); } } -export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { +export function ReadyView({ + cashouts, + routeCashoutDetails, +}: State.Ready): VNode { const { i18n, dateLocale } = useTranslationContext(); const resp = useConversionInfo(); if (!resp) { - return + return ; } if (resp instanceof TalerError) { - return + return ; } if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.NotImplemented: { - return - ; + return ( + + ); } - default: assertUnreachable(resp.case) + default: + assertUnreachable(resp.case); } } - if (!cashouts.length) return
- const txByDate = cashouts.reduce((prev, cur) => { - const d = cur.creation_time.t_s === "never" - ? "" - : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", { locale: dateLocale }) - if (!prev[d]) { - prev[d] = [] - } - prev[d].push(cur) - return prev - }, {} as Record) + if (!cashouts.length) return
; + const txByDate = cashouts.reduce( + (prev, cur) => { + const d = + cur.creation_time.t_s === "never" + ? "" + : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", { + locale: dateLocale, + }); + if (!prev[d]) { + prev[d] = []; + } + prev[d].push(cur); + return prev; + }, + {} as Record, + ); return (
-

Latest cashouts

+

+ Latest cashouts +

- - - - + + + + {Object.entries(txByDate).map(([date, txs], idx) => { - return - - - - {txs.map(item => { - const creationTime = item.creation_time.t_s === "never" ? "" : format(item.creation_time.t_s * 1000, "HH:mm:ss", { locale: dateLocale }) - return ( - - + + + {txs.map((item) => { + const creationTime = + item.creation_time.t_s === "never" + ? "" + : format(item.creation_time.t_s * 1000, "HH:mm:ss", { + locale: dateLocale, + }); + return ( + + + - - - - - ) - })} - + + + + + + + ); + })} + + ); })} -
{i18n.str`Created`}{i18n.str`Created`}
- {date} -
{ - e.preventDefault(); - onSelected(item.id); - }} class="relative py-2 pl-2 pr-2 text-sm "> -
{creationTime}
- {//FIXME: implement responsive view - } - {/*
+ return ( + +
+ {date} +
+
+ {creationTime} +
+ { + //FIXME: implement responsive view + } + {/*
Amount
{item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( @@ -127,34 +181,33 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
*/} -
{ - e.preventDefault(); - onSelected(item.id); - }} class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer"> { - e.preventDefault(); - onSelected(item.id); - }} class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer"> { - e.preventDefault(); - onSelected(item.id); - }} class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md"> - {item.subject} -
- -
); - } diff --git a/packages/demobank-ui/src/components/EmptyComponentExample/state.ts b/packages/demobank-ui/src/components/EmptyComponentExample/state.ts index c63f7d3ab..057664983 100644 --- a/packages/demobank-ui/src/components/EmptyComponentExample/state.ts +++ b/packages/demobank-ui/src/components/EmptyComponentExample/state.ts @@ -17,7 +17,7 @@ // import { wxApi } from "../../wxApi.js"; import { Props, State } from "./index.js"; -export function useComponentState({ p }: Props): State { +export function useComponentState({ p: _p }: Props): State { return { status: "ready", error: undefined, diff --git a/packages/demobank-ui/src/components/EmptyComponentExample/views.tsx b/packages/demobank-ui/src/components/EmptyComponentExample/views.tsx index c32e25b50..457933a5f 100644 --- a/packages/demobank-ui/src/components/EmptyComponentExample/views.tsx +++ b/packages/demobank-ui/src/components/EmptyComponentExample/views.tsx @@ -15,20 +15,11 @@ */ import { h, VNode } from "preact"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { State } from "./index.js"; -export function LoadingUriView({ error }: State.LoadingUriError): VNode { - const { i18n } = useTranslationContext(); - - return ( -
-
- ); +export function LoadingUriView(): VNode { + return
; } -export function ReadyView({ error }: State.Ready): VNode { - const { i18n } = useTranslationContext(); - +export function ReadyView(): VNode { return
; } diff --git a/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx index 25e79e9e0..8679af050 100644 --- a/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx +++ b/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx @@ -1,3 +1,18 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ import { TalerError } from "@gnu-taler/taler-util"; import { ErrorLoading } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -5,5 +20,5 @@ import { usePreferences } from "../hooks/preferences.js"; export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode { const [pref] = usePreferences(); - return + return ; } diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts index 721fede45..56eaefb8d 100644 --- a/packages/demobank-ui/src/components/Transactions/state.ts +++ b/packages/demobank-ui/src/components/Transactions/state.ts @@ -14,7 +14,12 @@ GNU Taler; see the file COPYING. If not, see */ -import { AbsoluteTime, Amounts, TalerError, parsePaytoUri } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + Amounts, + TalerError, + parsePaytoUri, +} from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/access.js"; import { Props, State, Transaction } from "./index.js"; @@ -33,29 +38,38 @@ export function useComponentState({ account }: Props): State { }; } - const transactions = result.data.type === "fail" ? [] : result.data.body.transactions - .map((tx) => { + const transactions = + result.data.type === "fail" + ? [] + : result.data.body.transactions + .map((tx) => { + const negative = tx.direction === "debit"; + const cp = parsePaytoUri( + negative ? tx.creditor_payto_uri : tx.debtor_payto_uri, + ); + const counterpart = + (cp === undefined || !cp.isKnown + ? undefined + : cp.targetType === "iban" + ? cp.iban + : cp.targetType === "x-taler-bank" + ? cp.account + : cp.targetType === "bitcoin" + ? `${cp.targetPath.substring(0, 6)}...` + : undefined) ?? "unkown"; - const negative = tx.direction === "debit"; - const cp = parsePaytoUri(negative ? tx.creditor_payto_uri : tx.debtor_payto_uri); - const counterpart = (cp === undefined || !cp.isKnown ? undefined : - cp.targetType === "iban" ? cp.iban : - cp.targetType === "x-taler-bank" ? cp.account : - cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ?? - "unkown"; - - const when = AbsoluteTime.fromProtocolTimestamp(tx.date); - const amount = Amounts.parse(tx.amount); - const subject = tx.subject; - return { - negative, - counterpart, - when, - amount, - subject, - }; - }) - .filter((x): x is Transaction => x !== undefined); + const when = AbsoluteTime.fromProtocolTimestamp(tx.date); + const amount = Amounts.parse(tx.amount); + const subject = tx.subject; + return { + negative, + counterpart, + when, + amount, + subject, + }; + }) + .filter((x): x is Transaction => x !== undefined); return { status: "ready", diff --git a/packages/demobank-ui/src/components/Transactions/test.ts b/packages/demobank-ui/src/components/Transactions/test.ts index 0b5e2bfbf..cf33a0b1c 100644 --- a/packages/demobank-ui/src/components/Transactions/test.ts +++ b/packages/demobank-ui/src/components/Transactions/test.ts @@ -19,14 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ErrorType } from "@gnu-taler/web-util/browser"; +import { TalerErrorCode } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { SwrMockEnvironment } from "@gnu-taler/web-util/testing"; import { expect } from "chai"; import { TRANSACTION_API_EXAMPLE } from "../../endpoints.js"; import { Props } from "./index.js"; import { useComponentState } from "./state.js"; -import { HttpStatusCode, TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; describe("Transaction states", () => { it.skip("should query backend and render transactions", async () => { @@ -36,7 +35,6 @@ describe("Transaction states", () => { account: "myAccount", }; - //@ts-ignore env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, { response: { data: { @@ -183,10 +181,15 @@ describe("Transaction states", () => { }, ({ status, error }) => { expect(status).equals("loading-error"); - if (error === undefined || !error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + if ( + error === undefined || + !error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED) + ) { throw Error("not the expected error"); } - expect(error.errorDetail.code).deep.equal(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED); + expect(error.errorDetail.code).deep.equal( + TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, + ); }, ], env.buildTestingContext(), diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index 7b3c77fa2..1d63cc2cb 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -20,99 +20,179 @@ import { Fragment, h, VNode } from "preact"; import { useBankCoreApiContext } from "../../context/config.js"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; +import { privatePages } from "../../Routing.js"; - -export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode { +export function ReadyView({ + transactions, + onNext, + onPrev, +}: State.Ready): VNode { const { i18n, dateLocale } = useTranslationContext(); const { config } = useBankCoreApiContext(); - if (!transactions.length) return
- const txByDate = transactions.reduce((prev, cur) => { - const d = cur.when.t_ms === "never" - ? "" - : format(cur.when.t_ms, "dd/MM/yyyy", { locale: dateLocale }) - if (!prev[d]) { - prev[d] = [] - } - prev[d].push(cur) - return prev - }, {} as Record) + if (!transactions.length) return
; + const txByDate = transactions.reduce( + (prev, cur) => { + const d = + cur.when.t_ms === "never" + ? "" + : format(cur.when.t_ms, "dd/MM/yyyy", { locale: dateLocale }); + if (!prev[d]) { + prev[d] = []; + } + prev[d].push(cur); + return prev; + }, + {} as Record, + ); return (
-

Latest transactions

+

+ Latest transactions +

- - - - + + + + {Object.entries(txByDate).map(([date, txs], idx) => { - return - - - - {txs.map(item => { - const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss", { locale: dateLocale }) - return ( - + + + {txs.map((item) => { + const time = + item.when.t_ms === "never" + ? "" + : format(item.when.t_ms, "HH:mm:ss", { + locale: dateLocale, + }); + return ( + + + + - - - - ) - })} - - + + + + ); + })} + + ); })} -
{i18n.str`Date`}{i18n.str`Date`}
- {date} -
-
{time}
-
-
Amount
-
- {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( - - - - ) : ( - <{i18n.str`invalid value`}> - )}
+ return ( + +
+ {date} +
+
{time}
+
+
+ Amount +
+
+ {item.negative + ? i18n.str`sent` + : i18n.str`received`}{" "} + {item.amount ? ( + + + + ) : ( + + <{i18n.str`invalid value`}> + + )} +
-
Counterpart
-
- {item.negative ? i18n.str`to` : i18n.str`from`} +
+ Counterpart +
+
+ {item.negative ? i18n.str`to` : i18n.str`from`}{" "} + + {item.counterpart} + +
+
+
+                                {item.subject}
+                              
+
+
+
-
- - - -
- {account && -
-
-

- - -

-
-
- } - -
-
- {children} -
+ }, [error]); + + return ( +
+
+
{ + backend.logOut(); + resetBankState(); + } + } + sites={ + !settings.topNavSites ? [] : Object.entries(settings.topNavSites) + } + supportedLangs={["en", "es", "de"]} + > +
  • +
    + Preferences +
    +
      + {getAllBooleanPreferences().map((set) => { + const isOn: boolean = !!preferences[set]; + return ( +
    • +
      + + + {getLabelForPreferences(set, i18n)} + + + +
      +
    • + ); + })} +
    +
  • +
    -
    - -
    - -
    + + +
    + {account && ( +
    +
    +

    + + + + + + +

    +
    +
    + )} + +
    +
    + {children} +
    +
    +
    + +
    +
    ); } function WelcomeAccount({ account: accountName }: { account: string }): VNode { const { i18n } = useTranslationContext(); - return - Welcome, {accountName} - + return ( + + + Welcome, {accountName} + + + ); } function AccountBalance({ account }: { account: string }): VNode { const result = useAccountDetails(account); const { config } = useBankCoreApiContext(); if (!result) { - return + return ; } if (result instanceof TalerError) { - return
    + return
    ; } - if (result.type === "fail") return
    + if (result.type === "fail") return
    ; - return + return ( + + ); } diff --git a/packages/demobank-ui/src/pages/DownloadStats.tsx b/packages/demobank-ui/src/pages/DownloadStats.tsx index 48daacaea..a98c573ae 100644 --- a/packages/demobank-ui/src/pages/DownloadStats.tsx +++ b/packages/demobank-ui/src/pages/DownloadStats.tsx @@ -14,18 +14,28 @@ GNU Taler; see the file COPYING. If not, see */ -import { AccessToken, AmountString, Logger, TalerCoreBankHttpClient, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; -import { Attention, LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { + AccessToken, + AmountString, + TalerCoreBankHttpClient, + TalerCorebankApi, + TalerError, +} from "@gnu-taler/taler-util"; +import { + Attention, + LocalNotificationBanner, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; +import { RouteDefinition } from "../route.js"; import { getTimeframesForDate } from "./admin/AdminHome.js"; -const logger = new Logger("PublicHistoriesPage"); - interface Props { - onCancel: () => void; + routeCancel: RouteDefinition>; } type Options = { @@ -36,16 +46,19 @@ type Options = { compareWithPrevious: boolean; endOnFirstFail: boolean; includeHeader: boolean; -} +}; -/** +/** * Show histories of public accounts. */ -export function DownloadStats({ onCancel }: Props): VNode { +export function DownloadStats({ routeCancel }: Props): VNode { const { i18n } = useTranslationContext(); const { state: credentials } = useBackendState(); - const creds = credentials.status !== "loggedIn" || !credentials.isUserAdministrator ? undefined : credentials + const creds = + credentials.status !== "loggedIn" || !credentials.isUserAdministrator + ? undefined + : credentials; const { api } = useBankCoreApiContext(); const [options, setOptions] = useState({ @@ -56,19 +69,18 @@ export function DownloadStats({ onCancel }: Props): VNode { includeHeader: true, monthMetric: true, yearMetric: true, - }) - const [lastStep, setLastStep] = useState<{ step: number, total: number }>() - const [downloaded, setDownloaded] = useState() - const referenceDates = [new Date()] - const [notification, notify, handleError] = useLocalNotification() + }); + const [lastStep, setLastStep] = useState<{ step: number; total: number }>(); + const [downloaded, setDownloaded] = useState(); + const referenceDates = [new Date()]; + const [notification, , handleError] = useLocalNotification(); if (!creds) { - return
    only admin can download stats
    + return
    only admin can download stats
    ; } return (
    -
    @@ -78,13 +90,12 @@ export function DownloadStats({ onCancel }: Props): VNode {
    -
    { - e.preventDefault() + onSubmit={(e) => { + e.preventDefault(); }} >
    @@ -92,223 +103,393 @@ export function DownloadStats({ onCancel }: Props): VNode {
    - + Include hour metric -
    - + Include day metric -
    - + Include month metric -
    - + Include year metric -
    - + Include table header -
    - - Add previous metric for compare + + + Add previous metric for compare + -
    - + Fail on first error -
    - -
    - -
    -
    - {!lastStep || lastStep.step === lastStep.total ?
    :
    -
    -
    - - downloading... {Math.round((((lastStep.step / lastStep.total)) * 100))} - + {!lastStep || lastStep.step === lastStep.total ? ( +
    + ) : ( +
    +
    +
    + + + downloading...{" "} + {Math.round((lastStep.step / lastStep.total) * 100)} + + +
    -
    } - {!downloaded ?
    : - + )} + {!downloaded ? ( + ); } - -async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken, options: Options, references: Date[], progres: (current: number, total: number) => void): Promise { +async function fetchAllStatus( + api: TalerCoreBankHttpClient, + token: AccessToken, + options: Options, + references: Date[], + progres: (current: number, total: number) => void, +): Promise { const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = []; if (options.hourMetric) { - allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour) + allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour); } if (options.dayMetric) { - allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day) + allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day); } if (options.monthMetric) { - allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month) + allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month); } if (options.yearMetric) { - allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year) + allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year); } /** * conver request into frames */ - const allFrames = allMetrics.flatMap(timeframe => references.map(reference => ({ - reference, - timeframe, - moment: getTimeframesForDate(reference, timeframe) - })) - ) - const total = allFrames.length + const allFrames = allMetrics.flatMap((timeframe) => + references.map((reference) => ({ + reference, + timeframe, + moment: getTimeframesForDate(reference, timeframe), + })), + ); + const total = allFrames.length; /** * call API for info */ - const allInfo = await allFrames.reduce(async (prev, frame, index) => { - const accumulatedMap = await prev - progres(index, total) - // await delay() - const previous = options.compareWithPrevious ? (await api.getMonitor(token, { - timeframe: frame.timeframe, - which: frame.moment.previous - })) : undefined - - if (previous && previous.type === "fail" && options.endOnFirstFail) { - throw TalerError.fromUncheckedDetail(previous.detail) - } + const allInfo = await allFrames.reduce( + async (prev, frame, index) => { + const accumulatedMap = await prev; + progres(index, total); + // await delay() + const previous = options.compareWithPrevious + ? await api.getMonitor(token, { + timeframe: frame.timeframe, + which: frame.moment.previous, + }) + : undefined; + + if (previous && previous.type === "fail" && options.endOnFirstFail) { + throw TalerError.fromUncheckedDetail(previous.detail); + } - const current = await api.getMonitor(token, { - timeframe: frame.timeframe, - which: frame.moment.current - }) + const current = await api.getMonitor(token, { + timeframe: frame.timeframe, + which: frame.moment.current, + }); - if (current.type === "fail" && options.endOnFirstFail) { - throw TalerError.fromUncheckedDetail(current.detail) - } + if (current.type === "fail" && options.endOnFirstFail) { + throw TalerError.fromUncheckedDetail(current.detail); + } - const metricName = TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]] - accumulatedMap[metricName] = { - reference: frame.reference, - current: current.type !== "ok" ? undefined : current.body, - previous: !previous || previous.type !== "ok" ? undefined : previous.body, - } - return accumulatedMap - }, Promise.resolve({} as Record)) - progres(total, total) + const metricName = + TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]]; + accumulatedMap[metricName] = { + reference: frame.reference, + current: current.type !== "ok" ? undefined : current.body, + previous: + !previous || previous.type !== "ok" ? undefined : previous.body, + }; + return accumulatedMap; + }, + Promise.resolve({} as Record), + ); + progres(total, total); /** * conver into table format - * + * */ const table: Array = []; if (options.includeHeader) { - table.push(["date", + table.push([ + "date", "metric", "reference", "talerInCount", @@ -320,7 +501,8 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken, "cashinRegionalVolume", "cashoutCount", "cashoutFiatVolume", - "cashoutRegionalVolume",]) + "cashoutRegionalVolume", + ]); } Object.entries(allInfo).forEach(([name, data]) => { if (data.current) { @@ -328,9 +510,9 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken, date: data.reference.getTime(), metric: name, reference: "current", - ...dataToRow(data.current) - } - table.push((Object.values(row) as string[])) + ...dataToRow(data.current), + }; + table.push(Object.values(row) as string[]); } if (data.previous) { @@ -338,20 +520,20 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken, date: data.reference.getTime(), metric: name, reference: "previous", - ...dataToRow(data.previous) - } - table.push((Object.values(row) as string[])) + ...dataToRow(data.previous), + }; + table.push(Object.values(row) as string[]); } - }) + }); const csv = table.reduce((acc, row) => { - return acc + row.join(",") + "\n" - }, "") + return acc + row.join(",") + "\n"; + }, ""); - return csv + return csv; } -type JustData = Omit, "date">, "reference"> +type JustData = Omit, "date">, "reference">; function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData { return { talerInCount: info.talerInCount, @@ -359,23 +541,28 @@ function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData { talerOutCount: info.talerOutCount, talerOutVolume: info.talerOutVolume, cashinCount: info.type === "no-conversions" ? undefined : info.cashinCount, - cashinFiatVolume: info.type === "no-conversions" ? undefined : info.cashinFiatVolume, - cashinRegionalVolume: info.type === "no-conversions" ? undefined : info.cashinRegionalVolume, - cashoutCount: info.type === "no-conversions" ? undefined : info.cashoutCount, - cashoutFiatVolume: info.type === "no-conversions" ? undefined : info.cashoutFiatVolume, - cashoutRegionalVolume: info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume, - } + cashinFiatVolume: + info.type === "no-conversions" ? undefined : info.cashinFiatVolume, + cashinRegionalVolume: + info.type === "no-conversions" ? undefined : info.cashinRegionalVolume, + cashoutCount: + info.type === "no-conversions" ? undefined : info.cashoutCount, + cashoutFiatVolume: + info.type === "no-conversions" ? undefined : info.cashoutFiatVolume, + cashoutRegionalVolume: + info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume, + }; } type Data = { - reference: Date, + reference: Date; previous: TalerCorebankApi.MonitorResponse | undefined; current: TalerCorebankApi.MonitorResponse | undefined; -} +}; type TableRow = { - date: number, - metric: string, - reference: "current" | "previous", + date: number; + metric: string; + reference: "current" | "previous"; cashinCount?: number; cashinRegionalVolume?: AmountString; cashinFiatVolume?: AmountString; @@ -386,11 +573,4 @@ type TableRow = { talerInVolume: AmountString; talerOutCount: number; talerOutVolume: AmountString; -} -async function delay() { - return new Promise(res => { - setTimeout(() => { - res(null) - }, 500) - }) -} \ No newline at end of file +}; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 04bf0b7fa..7e5631cfb 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -14,29 +14,48 @@ GNU Taler; see the file COPYING. If not, see */ -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { LocalNotificationBanner, ShowInputErrorLabel, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { + HttpStatusCode, + TranslatedString, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + LocalNotificationBanner, + ShowInputErrorLabel, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { undefinedIfEmpty } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; - +import { RouteDefinition } from "../route.js"; /** * Collect and submit login data. */ -export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: boolean, currentUser?: string, onRegister?: () => void }): VNode { +export function LoginForm({ + currentUser, + fixedUser, + routeRegister, +}: { + fixedUser?: boolean; + currentUser?: string; + routeRegister?: RouteDefinition>; +}): VNode { const backend = useBackendState(); - const sessionUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined - const [username, setUsername] = useState(currentUser ?? sessionUser); + const sessionUser = + backend.state.status !== "loggedOut" ? backend.state.username : undefined; + const [username, setUsername] = useState( + currentUser ?? sessionUser, + ); const [password, setPassword] = useState(); const { i18n } = useTranslationContext(); const { api } = useBankCoreApiContext(); - const [notification, notify, handleError] = useLocalNotification() + const [notification, notify, handleError] = useLocalNotification(); const { config } = useBankCoreApiContext(); const ref = useRef(null); @@ -44,63 +63,71 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: ref.current?.focus(); }, []); - const [busy, setBusy] = useState>() + const [busy, setBusy] = useState>(); - const errors = undefinedIfEmpty({ - username: !username - ? i18n.str`Missing username` - // : !USERNAME_REGEX.test(username) - // ? i18n.str`Use letters and numbers only, and start with a lowercase letter` - : undefined, - password: !password ? i18n.str`Missing password` : undefined, - }) ?? busy; + const errors = + undefinedIfEmpty({ + username: !username + ? i18n.str`Missing username` + : // : !USERNAME_REGEX.test(username) + // ? i18n.str`Use letters and numbers only, and start with a lowercase letter` + undefined, + password: !password ? i18n.str`Missing password` : undefined, + }) ?? busy; async function doLogout() { - backend.logOut() + backend.logOut(); } async function doLogin() { if (!username || !password) return; - setBusy({}) + setBusy({}); await handleError(async () => { - const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { - // scope: "readwrite" as "write", //FIX: different than merchant - scope: "readwrite", - duration: { - d_us: "forever" //FIX: should return shortest - // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, - refreshable: true, - }) + const resp = await api + .getAuthenticationAPI(username) + .createAccessToken(password, { + // scope: "readwrite" as "write", // FIX: different than merchant + scope: "readwrite", + duration: { + d_us: "forever", // FIX: should return shortest + // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 + }, + refreshable: true, + }); if (resp.type === "ok") { backend.logIn({ username, token: resp.body.access_token }); } else { switch (resp.case) { - case HttpStatusCode.Unauthorized: return notify({ - type: "error", - title: i18n.str`Wrong credentials for "${username}"`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`Account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${username}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: + assertUnreachable(resp); } } - }) + }); setPassword(undefined); - setBusy(undefined) + setBusy(undefined); } return (
    -
    { e.preventDefault(); }} @@ -108,7 +135,10 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: autoCorrect="off" >
    -
    ); diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts index 18ffe0ec3..20cb1760f 100644 --- a/packages/demobank-ui/src/pages/OperationState/index.ts +++ b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -14,28 +14,46 @@ GNU Taler; see the file COPYING. If not, see */ -import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerError, WithdrawUriResult } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + AmountJson, + TalerCoreBankErrorsByMethod, + TalerError, + WithdrawUriResult, +} from "@gnu-taler/taler-util"; import { Loading, utils } from "@gnu-taler/web-util/browser"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; import { useComponentState } from "./state.js"; -import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js"; +import { + AbortedView, + ConfirmedView, + FailedView, + InvalidPaytoView, + InvalidReserveView, + InvalidWithdrawalView, + NeedConfirmationView, + ReadyView, +} from "./views.js"; +import { RouteDefinition } from "../../route.js"; export interface Props { currency: string; - onAuthorizationRequired: () => void, - onClose: () => void; + onAuthorizationRequired: () => void; + routeClose: RouteDefinition>; + onAbort: () => void; } -export type State = State.Loading | - State.LoadingError | - State.Ready | - State.Failed | - State.Aborted | - State.Confirmed | - State.InvalidPayto | - State.InvalidWithdrawal | - State.InvalidReserve | - State.NeedConfirmation; +export type State = + | State.Loading + | State.LoadingError + | State.Ready + | State.Failed + | State.Aborted + | State.Confirmed + | State.InvalidPayto + | State.InvalidWithdrawal + | State.InvalidReserve + | State.NeedConfirmation; export namespace State { export interface Loading { @@ -59,48 +77,55 @@ export namespace State { export interface Ready { status: "ready"; error: undefined; - uri: WithdrawUriResult, - onClose: () => Promise | undefined>; + uri: WithdrawUriResult; + onAbort: () => Promise< + TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined + >; + routeClose: RouteDefinition>; } export interface InvalidPayto { - status: "invalid-payto", + status: "invalid-payto"; error: undefined; payto: string | undefined; - onClose: () => void; } export interface InvalidWithdrawal { - status: "invalid-withdrawal", + status: "invalid-withdrawal"; error: undefined; - onClose: () => void; - uri: string, + uri: string; } export interface InvalidReserve { - status: "invalid-reserve", + status: "invalid-reserve"; error: undefined; - onClose: () => void; reserve: string | undefined; } export interface NeedConfirmation { - status: "need-confirmation", - onAuthorizationRequired: () => void, - account: string, - onAbort: undefined | (() => Promise | undefined>); - onConfirm: undefined | (() => Promise | undefined>); + status: "need-confirmation"; + onAuthorizationRequired: () => void; + account: string; + onAbort: + | undefined + | (() => Promise< + TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined + >); + onConfirm: + | undefined + | (() => Promise< + TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined + >); error: undefined; - id: string, + id: string; } export interface Aborted { - status: "aborted", + status: "aborted"; error: undefined; - onClose: () => void; + routeClose: RouteDefinition>; } export interface Confirmed { - status: "confirmed", + status: "confirmed"; error: undefined; - onClose: () => void; + routeClose: RouteDefinition>; } - } export interface Transaction { @@ -113,13 +138,13 @@ export interface Transaction { const viewMapping: utils.StateViewMap = { loading: Loading, - "failed": FailedView, + failed: FailedView, "invalid-payto": InvalidPaytoView, "invalid-withdrawal": InvalidWithdrawalView, "invalid-reserve": InvalidReserveView, "need-confirmation": NeedConfirmationView, - "aborted": AbortedView, - "confirmed": ConfirmedView, + aborted: AbortedView, + confirmed: ConfirmedView, "loading-error": ErrorLoadingWithDebug, ready: ReadyView, }; diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 32a13d047..20d66bbb1 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -14,7 +14,16 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, HttpStatusCode, TalerCoreBankErrorsByMethod, TalerError, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; +import { + Amounts, + HttpStatusCode, + TalerCoreBankErrorsByMethod, + TalerError, + assertUnreachable, + parsePaytoUri, + parseWithdrawUri, + stringifyWithdrawUri, +} from "@gnu-taler/taler-util"; import { utils } from "@gnu-taler/web-util/browser"; import { useEffect, useState } from "preact/hooks"; import { mutate } from "swr"; @@ -23,73 +32,80 @@ import { useWithdrawalDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; import { useBankState } from "../../hooks/bank-state.js"; import { usePreferences } from "../../hooks/preferences.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { Props, State } from "./index.js"; -export function useComponentState({ currency, onClose, onAuthorizationRequired, }: Props): utils.RecursiveState { - const [settings] = usePreferences() +export function useComponentState({ + currency, + routeClose, + onAbort, + onAuthorizationRequired, +}: Props): utils.RecursiveState { + const [settings] = usePreferences(); const [bankState, updateBankState] = useBankState(); - const { state: credentials } = useBackendState() - const creds = credentials.status !== "loggedIn" ? undefined : credentials - const { api } = useBankCoreApiContext() + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials; + const { api } = useBankCoreApiContext(); - const [failure, setFailure] = useState | undefined>() - const amount = settings.maxWithdrawalAmount + const [failure, setFailure] = useState< + TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined + >(); + const amount = settings.maxWithdrawalAmount; async function doSilentStart() { - //FIXME: if amount is not enough use balance - const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) + // FIXME: if amount is not enough use balance + const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`); if (!creds) return; const resp = await api.createWithdrawal(creds, { amount: Amounts.stringify(parsedAmount), }); if (resp.type === "fail") { - setFailure(resp) + setFailure(resp); return; } - updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id) - + updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id); } - const withdrawalOperationId = bankState.currentWithdrawalOperationId + const withdrawalOperationId = bankState.currentWithdrawalOperationId; useEffect(() => { if (withdrawalOperationId === undefined) { - doSilentStart() + doSilentStart(); } - }, [settings.fastWithdrawal, amount]) + }, [settings.fastWithdrawal, amount]); if (failure) { return { status: "failed", - error: failure - } + error: failure, + }; } if (!withdrawalOperationId) { return { status: "loading", - error: undefined - } + error: undefined, + }; } - const wid = withdrawalOperationId + const wid = withdrawalOperationId; async function doAbort() { if (!creds) return; const resp = await api.abortWithdrawalById(creds, wid); if (resp.type === "ok") { - updateBankState("currentWithdrawalOperationId", undefined) - onClose(); + // updateBankState("currentWithdrawalOperationId", undefined) + onAbort(); } else { return resp; } } - async function doConfirm(): Promise | undefined> { + async function doConfirm(): Promise< + TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined + > { if (!creds) return; const resp = await api.confirmWithdrawalById(creds, wid); if (resp.type === "ok") { - mutate(() => true)//clean withdrawal state + mutate(() => true); //clean withdrawal state } else { return resp; } @@ -105,30 +121,29 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, status: "invalid-withdrawal", error: undefined, uri, - onClose, - } + }; } return (): utils.RecursiveState => { const result = useWithdrawalDetails(withdrawalOperationId); - const shouldCreateNewOperation = result && !(result instanceof TalerError) + const shouldCreateNewOperation = result && !(result instanceof TalerError); useEffect(() => { if (shouldCreateNewOperation) { - doSilentStart() + doSilentStart(); } - }, []) + }, []); if (!result) { return { status: "loading", - error: undefined - } + error: undefined, + }; } if (result instanceof TalerError) { return { status: "loading-error", - error: result - } + error: result, + }; } if (result.type === "fail") { @@ -138,13 +153,11 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, return { status: "aborted", error: undefined, - onClose: async () => { - updateBankState("currentWithdrawalOperationId", undefined) - onClose() - }, - } + routeClose, + }; } - default: assertUnreachable(result) + default: + assertUnreachable(result); } } @@ -153,26 +166,20 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, return { status: "aborted", error: undefined, - onClose: async () => { - updateBankState("currentWithdrawalOperationId", undefined) - onClose() - }, - } + routeClose, + }; } if (data.status === "confirmed") { if (!settings.showWithdrawalSuccess) { - updateBankState("currentWithdrawalOperationId", undefined) - onClose() + updateBankState("currentWithdrawalOperationId", undefined); + // onClose() } return { status: "confirmed", error: undefined, - onClose: async () => { - updateBankState("currentWithdrawalOperationId", undefined) - onClose() - }, - } + routeClose, + }; } if (data.status === "pending") { @@ -180,11 +187,14 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, status: "ready", error: undefined, uri: parsedUri, - onClose: !creds ? (async () => { - onClose(); - return undefined - }) : doAbort, - } + routeClose, + onAbort: !creds + ? async () => { + onAbort(); + return undefined; + } + : doAbort, + }; } if (!data.selected_reserve_pub) { @@ -192,19 +202,19 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, status: "invalid-reserve", error: undefined, reserve: data.selected_reserve_pub, - onClose, - } + }; } - const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + const account = !data.selected_exchange_account + ? undefined + : parsePaytoUri(data.selected_exchange_account); if (!account) { return { status: "invalid-payto", error: undefined, payto: data.selected_exchange_account, - onClose, - } + }; } return { @@ -214,8 +224,7 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired, account: data.username, id: withdrawalOperationId, onAbort: !creds ? undefined : doAbort, - onConfirm: !creds ? undefined : doConfirm - } - } - + onConfirm: !creds ? undefined : doConfirm, + }; + }; } diff --git a/packages/demobank-ui/src/pages/OperationState/test.ts b/packages/demobank-ui/src/pages/OperationState/test.ts index 3ba351cd3..d47cb64a2 100644 --- a/packages/demobank-ui/src/pages/OperationState/test.ts +++ b/packages/demobank-ui/src/pages/OperationState/test.ts @@ -19,14 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import * as tests from "@gnu-taler/web-util/testing"; -import { SwrMockEnvironment } from "@gnu-taler/web-util/testing"; -import { expect } from "chai"; -import { CASHOUT_API_EXAMPLE } from "../../endpoints.js"; -import { Props } from "./index.js"; -import { useComponentState } from "./state.js"; +// import * as tests from "@gnu-taler/web-util/testing"; +// import { SwrMockEnvironment } from "@gnu-taler/web-util/testing"; +// import { expect } from "chai"; +// import { CASHOUT_API_EXAMPLE } from "../../endpoints.js"; +// import { Props } from "./index.js"; +// import { useComponentState } from "./state.js"; describe("Withdrawal operation states", () => { - it("should do some tests", async () => { - }); + it("should do some tests", async () => {}); }); diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index c86b8bd4b..ac3724eb8 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,121 +14,143 @@ GNU Taler; see the file COPYING. If not, see */ -import { AbsoluteTime, HttpStatusCode, TalerErrorCode, TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AbsoluteTime, + HttpStatusCode, + TalerErrorCode, + TranslatedString, + assertUnreachable, + stringifyWithdrawUri, +} from "@gnu-taler/taler-util"; +import { + Attention, + LocalNotificationBanner, + notifyInfo, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../../components/QR.js"; import { useBankState } from "../../hooks/bank-state.js"; import { usePreferences } from "../../hooks/preferences.js"; import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { State } from "./index.js"; -export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { - return ( -
    Payto from server is not valid "{payto}"
    - ); +export function InvalidPaytoView({ payto }: State.InvalidPayto) { + return
    Payto from server is not valid "{payto}"
    ; } -export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { - return ( -
    Withdrawal uri from server is not valid "{uri}"
    - ); +export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) { + return
    Withdrawal uri from server is not valid "{uri}"
    ; } -export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { - return ( -
    Reserve from server is not valid "{reserve}"
    - ); +export function InvalidReserveView({ reserve }: State.InvalidReserve) { + return
    Reserve from server is not valid "{reserve}"
    ; } -export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, account, id, onAuthorizationRequired, }: State.NeedConfirmation) { - const { i18n } = useTranslationContext() - const [settings] = usePreferences() - const [notification, notify, errorHandler] = useLocalNotification() - const [, updateBankState] = useBankState() +export function NeedConfirmationView({ + onAbort: doAbort, + onConfirm: doConfirm, + account, + id, + onAuthorizationRequired, +}: State.NeedConfirmation) { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + const [notification, notify, errorHandler] = useLocalNotification(); + const [, updateBankState] = useBankState(); async function onCancel() { errorHandler(async () => { if (!doAbort) return; - const resp = await doAbort() + const resp = await doAbort(); if (!resp) return; switch (resp.case) { - case HttpStatusCode.Conflict: return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) + case HttpStatusCode.Conflict: + return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: + assertUnreachable(resp); } - }) + }); } async function onConfirm() { errorHandler(async () => { if (!doConfirm) return; - const resp = await doConfirm() + const resp = await doConfirm(); if (!resp) { if (!settings.showWithdrawalSuccess) { - notifyInfo(i18n.str`Wire transfer completed!`) + notifyInfo(i18n.str`Wire transfer completed!`); } - return + return; } switch (resp.case) { - case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return notify({ - type: "error", - title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({ - type: "error", - title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ - type: "error", - title: i18n.str`Your balance is not enough.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + return notify({ + type: "error", + title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return notify({ + type: "error", + title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return notify({ + type: "error", + title: i18n.str`Your balance is not enough.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); case HttpStatusCode.Accepted: { updateBankState("currentChallenge", { operation: "confirm-withdrawal", id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request: id, - }) - return onAuthorizationRequired() + }); + return onAuthorizationRequired(); } - default: assertUnreachable(resp) + default: + assertUnreachable(resp); } - }) + }); } return ( @@ -144,23 +166,27 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" autoCapitalize="none" autoCorrect="off" - onSubmit={e => { - e.preventDefault() + onSubmit={(e) => { + e.preventDefault(); }} >
    - - +
    - ); } export function FailedView({ error }: State.Failed) { const { i18n } = useTranslationContext(); switch (error.case) { - case HttpStatusCode.Unauthorized: return -
    - {error.detail.hint} -
    -
    - case HttpStatusCode.Conflict: return -
    - {error.detail.hint} -
    -
    - case HttpStatusCode.NotFound: return -
    - {error.detail.hint} -
    -
    - default: assertUnreachable(error) + case HttpStatusCode.Unauthorized: + return ( + +
    {error.detail.hint}
    +
    + ); + case HttpStatusCode.Conflict: + return ( + +
    {error.detail.hint}
    +
    + ); + case HttpStatusCode.NotFound: + return ( + +
    {error.detail.hint}
    +
    + ); + default: + assertUnreachable(error); } } -export function AbortedView({ error, onClose }: State.Aborted) { - return ( -
    aborted
    - ); +export function AbortedView() { + return
    aborted
    ; } -export function ConfirmedView({ error, onClose }: State.Confirmed) { +export function ConfirmedView({ routeClose }: State.Confirmed) { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = usePreferences() + const [settings, updateSettings] = usePreferences(); return ( -
    -
    -
    -

    - The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet. + The wire transfer to the Taler operator has been initiated. You + will soon receive the requested amount in your Taler wallet.

    @@ -234,132 +280,165 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) {
    - + Do not show this again -
    - +
    - ); } -export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> { +export function ReadyView({ + uri, + onAbort: doAbort, +}: State.Ready): VNode> { const { i18n } = useTranslationContext(); - const [notification, notify, errorHandler] = useLocalNotification() + const [notification, notify, errorHandler] = useLocalNotification(); const talerWithdrawUri = stringifyWithdrawUri(uri); useEffect(() => { - //Taler Wallet WebExtension is listening to headers response and tab updates. - //In the SPA there is no header response with the Taler URI so - //this hack manually triggers the tab update after the QR is in the DOM. + // Taler Wallet WebExtension is listening to headers response and tab updates. + // In the SPA there is no header response with the Taler URI so + // this hack manually triggers the tab update after the QR is in the DOM. // WebExtension will be using // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated document.title = `${document.title} ${uri.withdrawalOperationId}`; - const meta = document.createElement("meta") - meta.setAttribute("name", "taler-uri") - meta.setAttribute("content", talerWithdrawUri) - document.head.insertBefore(meta, document.head.children.length ? document.head.children[0] : null) + const meta = document.createElement("meta"); + meta.setAttribute("name", "taler-uri"); + meta.setAttribute("content", talerWithdrawUri); + document.head.insertBefore( + meta, + document.head.children.length ? document.head.children[0] : null, + ); }, []); - async function onClose() { + async function onAbort() { errorHandler(async () => { - const hasError = await doClose() + const hasError = await doAbort(); if (!hasError) return; switch (hasError.case) { - case HttpStatusCode.Conflict: return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - default: assertUnreachable(hasError) + case HttpStatusCode.Conflict: + return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + default: + assertUnreachable(hasError); } - }) + }); } - return - + return ( + + -
    - -
    +
    + +
    -
    -
    -

    - On this device -

    -
    -
    -

    - If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled. -

    -
    -
    - - Start - +
    +
    +

    + On this device +

    +
    +
    +

    + + If you are using a web browser on desktop you should access + your wallet with the GNU Taler WebExtension now or click the + link if your WebExtension have the "Inject Taler support" + option enabled. + +

    +
    +
    -
    -
    -
    -

    - On a mobile phone -

    -
    -
    -

    - Scan the QR code with your mobile device. -

    +
    +
    +

    + On a mobile phone +

    +
    +
    +

    + + Scan the QR code with your mobile device. + +

    +
    +
    +
    +
    -
    -
    -
    -
    - - - + + ); } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 55611c172..53086d4cc 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -15,30 +15,41 @@ */ import { AmountJson } from "@gnu-taler/taler-util"; -import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { useBankState } from "../hooks/bank-state.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; +import { RouteDefinition } from "../route.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; /** * Let the user choose a payment option, * then specify the details trigger the action. */ -export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationRequired }: { - limit: AmountJson, - onAuthorizationRequired: () => void, - goToConfirmOperation: (id: string) => void, +export function PaymentOptions({ + routeClose, + routeChargeWallet, + routeWireTransfer, + tab, + limit, + onOperationCreated, + onClose, + onAuthorizationRequired, +}: { + limit: AmountJson; + tab: "charge-wallet" | "wire-transfer" | undefined; + onAuthorizationRequired: () => void; + onOperationCreated: (wopid: string) => void; + onClose: () => void; + routeClose: RouteDefinition>; + routeChargeWallet: RouteDefinition>; + routeWireTransfer: RouteDefinition>; }): VNode { const { i18n } = useTranslationContext(); const [bankState] = useBankState(); - const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); - return (
    -
    Send money @@ -46,65 +57,112 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq
    {/* */} - + -
    - ) + ); } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 321b87253..2259929e7 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -17,68 +17,62 @@ import { AbsoluteTime, AmountJson, - AmountLike, AmountString, Amounts, CurrencySpecification, FRAC_SEPARATOR, HttpStatusCode, - Logger, PaytoString, TalerErrorCode, TranslatedString, + assertUnreachable, buildPayto, parsePaytoUri, - stringifyPaytoUri + stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { + LocalNotificationBanner, + ShowInputErrorLabel, + notifyInfo, useLocalNotification, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, Ref, VNode, h } from "preact"; +import { Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; -import { - undefinedIfEmpty, - validateIBAN, - withRuntimeErrorHandling -} from "../utils.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; import { useBankState } from "../hooks/bank-state.js"; - -const logger = new Logger("PaytoWireTransferForm"); +import { RouteDefinition } from "../route.js"; +import { undefinedIfEmpty, validateIBAN } from "../utils.js"; export function PaytoWireTransferForm({ focus, title, toAccount, onSuccess, - onCancel, + routeCancel, onAuthorizationRequired, limit, }: { - title: TranslatedString, + title: TranslatedString; focus?: boolean; - toAccount?: string, + toAccount?: string; onSuccess: () => void; onAuthorizationRequired: () => void; - onCancel: (() => void) | undefined; + routeCancel?: RouteDefinition>; limit: AmountJson; }): VNode { const [isRawPayto, setIsRawPayto] = useState(false); - const { state: credentials } = useBackendState() + const { state: credentials } = useBackendState(); const { api } = useBankCoreApiContext(); - const sendingToFixedAccount = toAccount !== undefined - //FIXME: support other destination that just IBAN + const sendingToFixedAccount = toAccount !== undefined; + // FIXME: support other destination that just IBAN const [iban, setIban] = useState(toAccount); const [subject, setSubject] = useState(); const [amount, setAmount] = useState(); - const [, updateBankState] = useBankState() + const [, updateBankState] = useBankState(); const [rawPaytoInput, rawPaytoInputSetter] = useState( undefined, @@ -89,7 +83,7 @@ export function PaytoWireTransferForm({ const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; - const [notification, notify, handleError] = useLocalNotification() + const [notification, notify, handleError] = useLocalNotification(); const errorsWire = undefinedIfEmpty({ iban: !iban @@ -135,18 +129,18 @@ export function PaytoWireTransferForm({ if (credentials.status !== "loggedIn") return; if (rawPaytoInput) { - const p = parsePaytoUri(rawPaytoInput) + const p = parsePaytoUri(rawPaytoInput); if (!p) return; - sendingAmount = p.params.amount as AmountString - delete p.params.amount - //if this payto is valid then it already have message - payto_uri = stringifyPaytoUri(p) + sendingAmount = p.params.amount as AmountString; + delete p.params.amount; + // if this payto is valid then it already have message + payto_uri = stringifyPaytoUri(p); } else { if (!iban || !subject) return; const ibanPayto = buildPayto("iban", iban, undefined); ibanPayto.params.message = encodeURIComponent(subject); payto_uri = stringifyPaytoUri(ibanPayto); - sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString + sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString; } const puri = payto_uri; const sAmount = sendingAmount; @@ -155,288 +149,348 @@ export function PaytoWireTransferForm({ const request = { payto_uri: puri, amount: sAmount, - } + }; const resp = await api.createTransaction(credentials, request); - mutate(() => true) + mutate(() => true); if (resp.type === "fail") { switch (resp.case) { - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.Unauthorized: return notify({ - type: "error", - title: i18n.str`Not enough permission to complete the operation.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_UNKNOWN_CREDITOR: return notify({ - type: "error", - title: i18n.str`The destination account "${puri}" was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_SAME_ACCOUNT: return notify({ - type: "error", - title: i18n.str`The origin and the destination of the transfer can't be the same.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ - type: "error", - title: i18n.str`Your balance is not enough.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The origin account "${puri}" was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Not enough permission to complete the operation.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_UNKNOWN_CREDITOR: + return notify({ + type: "error", + title: i18n.str`The destination account "${puri}" was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_SAME_ACCOUNT: + return notify({ + type: "error", + title: i18n.str`The origin and the destination of the transfer can't be the same.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return notify({ + type: "error", + title: i18n.str`Your balance is not enough.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The origin account "${puri}" was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); case HttpStatusCode.Accepted: { updateBankState("currentChallenge", { operation: "create-transaction", id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request, - }) - return onAuthorizationRequired() + }); + return onAuthorizationRequired(); } - default: assertUnreachable(resp) + default: + assertUnreachable(resp); } } + notifyInfo(i18n.str`Wire transfer created!`); onSuccess(); setAmount(undefined); setIban(undefined); setSubject(undefined); - rawPaytoInputSetter(undefined) - }) + rawPaytoInputSetter(undefined); + }); } - return (
    - {/** - * FIXME: Scan a qr code - */} -
    -

    - {title} -

    -
    -
    -