From dfd23f63ba40a2afb0cb41bf742b0ae647a2b38c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 22 Sep 2023 12:41:43 -0300 Subject: more ui --- .../demobank-ui/src/pages/OperationState/views.tsx | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 packages/demobank-ui/src/pages/OperationState/views.tsx (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx') diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx new file mode 100644 index 000000000..db25eaf61 --- /dev/null +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { Transactions } from "../../components/Transactions/index.js"; +import { PaymentOptions } from "../PaymentOptions.js"; +import { State } from "./index.js"; +import { CopyButton } from "../../components/CopyButton.js"; +import { bankUiSettings } from "../../settings.js"; +import { useBusinessAccountDetails } from "../../hooks/circuit.js"; +import { useSettings } from "../../hooks/settings.js"; + +export function InvalidPaytoView({ error }: State.InvalidPayto) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} +export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} +export function InvalidReserveView({ error }: State.InvalidReserve) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} + +export function NeedConfirmationView({ error }: State.NeedConfirmation) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} +export function AbortedView({ error }: State.Aborted) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} +export function ConfirmedView({ error }: State.Confirmed) { + return ( +
Payto from server is not valid "{error.data.paytoUri}"
+ ); +} + +export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { + const { i18n } = useTranslationContext(); + + return
+ +} -- cgit v1.2.3 From a59df74fb2b4374fd58f68fd4abaffe623cd54d6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 22 Sep 2023 15:29:19 -0300 Subject: more ui --- packages/demobank-ui/src/components/Routing.tsx | 27 +- packages/demobank-ui/src/hooks/settings.ts | 3 + .../demobank-ui/src/pages/AccountPage/index.ts | 2 + .../demobank-ui/src/pages/AccountPage/state.ts | 3 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 4 +- packages/demobank-ui/src/pages/BankFrame.tsx | 15 + packages/demobank-ui/src/pages/HomePage.tsx | 17 +- .../demobank-ui/src/pages/OperationState/index.ts | 10 +- .../demobank-ui/src/pages/OperationState/state.ts | 109 ++++++- .../demobank-ui/src/pages/OperationState/views.tsx | 363 ++++++++++++++++++++- packages/demobank-ui/src/pages/PaymentOptions.tsx | 17 +- packages/demobank-ui/src/pages/QrCodeSection.tsx | 104 ------ .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 273 ++++++++++------ .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 163 +-------- packages/demobank-ui/src/pages/business/Home.tsx | 1 - 15 files changed, 693 insertions(+), 418 deletions(-) (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index e1fd93737..90d2d4c48 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -45,6 +45,20 @@ export function Routing(): VNode { /> )} /> + ( + { + route("/account"); + }} + // onLoadNotOk={() => { + // route("/account"); + // }} + /> + )} + /> ( @@ -64,10 +78,6 @@ export function Routing(): VNode { return ( - ( @@ -76,9 +86,6 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} - // onLoadNotOk={() => { - // route("/account"); - // }} /> )} /> @@ -108,9 +115,9 @@ export function Routing(): VNode { } else { return { - // route(`/operation/${wopid}`); - // }} + goToConfirmOperation={(wopid) => { + route(`/operation/${wopid}`); + }} goToBusinessAccount={() => { route("/business"); }} diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index c2fd93a0c..5f004c6d4 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -30,6 +30,7 @@ interface Settings { currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; showDemoDescription: boolean; + showInstallWallet: boolean; maxWithdrawalAmount: number; fastWithdrawal: boolean; } @@ -39,6 +40,7 @@ export const codecForSettings = (): Codec => .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) .property("showDemoDescription", (codecForBoolean())) + .property("showInstallWallet", (codecForBoolean())) .property("fastWithdrawal", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); @@ -47,6 +49,7 @@ const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, showDemoDescription: true, + showInstallWallet: true, maxWithdrawalAmount: 25, fastWithdrawal: false, }; diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 128a6d30f..81eeb4a03 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -29,6 +29,7 @@ export interface Props { error: HttpResponsePaginated, ) => VNode; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; } export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound; @@ -54,6 +55,7 @@ export namespace State { account: string, limit: AmountJson, goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; } export interface InvalidIban { diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index a57e19901..1a1475c0d 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAccountDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; -export function useComponentState({ account, goToBusinessAccount }: Props): State { +export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State { const result = useAccountDetails(account); const backend = useBackendContext(); const { i18n } = useTranslationContext(); @@ -75,6 +75,7 @@ export function useComponentState({ account, goToBusinessAccount }: Props): Stat return { status: "ready", goToBusinessAccount, + goToConfirmOperation, error: undefined, account, limit, diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index 0187989af..23a815bd8 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -123,7 +123,7 @@ function ShowDemoInfo():VNode {
} -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); return @@ -131,7 +131,7 @@ export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): - + ; } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index d1c94135b..5bfaa63ec 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -206,6 +206,21 @@ export function BankFrame({ +
  • +
    + + + Show install wallet first + + + +
    +
  • diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 2acfc9b57..8d5e1f3b9 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -36,6 +36,7 @@ import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; +import { route } from "preact-router"; const logger = new Logger("AccountPage"); @@ -52,25 +53,20 @@ const logger = new Logger("AccountPage"); export function HomePage({ onRegister, account, - // onPendingOperationFound, + goToConfirmOperation, goToBusinessAccount, }: { account: string, - // onPendingOperationFound: (id: string) => void; onRegister: () => void; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; }): VNode { - const [settings] = useSettings(); const { i18n } = useTranslationContext(); - // if (settings.currentWithdrawalOperationId) { - // onPendingOperationFound(settings.currentWithdrawalOperationId); - // return ; - // } - return ( @@ -102,12 +98,13 @@ export function WithdrawalOperationPage({ ); return ; } - + return ( { updateSettings("currentWithdrawalOperationId", undefined) + onContinue() }} /> ); @@ -178,7 +175,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - + route("/") return
    error
    ; } return
    ; diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts index 254fcba5f..32302f272 100644 --- a/packages/demobank-ui/src/pages/OperationState/index.ts +++ b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -26,6 +26,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js"; export interface Props { currency: string; onClose: () => void; + goToConfirmOperation: (id: string) => void; } export type State = State.Loading | @@ -57,26 +58,33 @@ export namespace State { error: undefined; uri: WithdrawUriResult, onClose: () => void; + onAbort: () => void; } export interface InvalidPayto { status: "invalid-payto", error: undefined; payto: string | null; + onClose: () => void; } export interface InvalidWithdrawal { status: "invalid-withdrawal", error: undefined; + onClose: () => void; uri: string, } export interface InvalidReserve { status: "invalid-reserve", error: undefined; + onClose: () => void; reserve: string | null; } export interface NeedConfirmation { status: "need-confirmation", + onAbort: () => void; + onConfirm: () => void; error: undefined; + busy: boolean, } export interface Aborted { status: "aborted", @@ -111,7 +119,7 @@ const viewMapping: utils.StateViewMap = { ready: ReadyView, }; -export const AccountPage = utils.compose( +export const OperationState = utils.compose( (p: Props) => useComponentState(p), viewMapping, ); diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 6fb7bb28f..ae03ed529 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -15,21 +15,24 @@ */ import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; +import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; import { useBackendContext } from "../../context/backend.js"; -import { useAccessAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; +import { useAccessAPI, useAccessAnonAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; import { useSettings } from "../../hooks/settings.js"; -import { buildRequestErrorMessage } from "../../utils.js"; -import { useEffect } from "preact/hooks"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { getInitialBackendBaseURL } from "../../hooks/backend.js"; -export function useComponentState({ currency, onClose }: Props): utils.RecursiveState { +export function useComponentState({ currency, onClose,goToConfirmOperation }: Props): utils.RecursiveState { const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings() const { createWithdrawal } = useAccessAPI(); + const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI(); + const [busy, setBusy] = useState>() const amount = settings.maxWithdrawalAmount + async function doSilentStart() { //FIXME: if amount is not enough use balance const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) @@ -67,12 +70,14 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const withdrawalOperationId = settings.currentWithdrawalOperationId useEffect(() => { - doSilentStart() + if (withdrawalOperationId === undefined) { + doSilentStart() + } }, [settings.fastWithdrawal, amount]) const baseUrl = getInitialBackendBaseURL() - const withdrawalOperationId = settings.currentWithdrawalOperationId if (!withdrawalOperationId) { return { @@ -81,6 +86,63 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const wid = withdrawalOperationId + + async function doAbort() { + try { + setBusy({}) + await abortWithdrawal(wid); + onClose(); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The reserve operation has been confirmed previously and can't be aborted` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + setBusy(undefined) + } + + async function doConfirm() { + try { + setBusy({}) + await confirmWithdrawal(wid); + notifyInfo(i18n.str`Wire transfer completed!`) + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` + : status === HttpStatusCode.UnprocessableEntity + ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + setBusy(undefined) + } const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api` const uri = stringifyWithdrawUri({ bankIntegrationApiBaseUrl, @@ -92,11 +154,13 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "invalid-withdrawal", error: undefined, uri, + onClose, } } return (): utils.RecursiveState => { const result = useWithdrawalDetails(withdrawalOperationId); + if (!result.ok) { if (result.loading) { return { @@ -119,10 +183,17 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } if (data.confirmation_done) { + if (!settings.showWithdrawalSuccess) { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + } return { status: "confirmed", error: undefined, - onClose, + onClose: async () => { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, } } @@ -131,7 +202,12 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "ready", error: undefined, uri: parsedUri, - onClose + onClose: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + onAbort: doAbort, } } @@ -139,7 +215,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-reserve", error: undefined, - reserve: data.selected_reserve_pub + reserve: data.selected_reserve_pub, + onClose, } } @@ -149,13 +226,23 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-payto", error: undefined, - payto: data.selected_exchange_account + payto: data.selected_exchange_account, + onClose, } } + + // goToConfirmOperation(withdrawalOperationId) return { status: "need-confirmation", error: undefined, + onAbort: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + busy: !!busy, + onConfirm: doConfirm } } diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index db25eaf61..17f1d8457 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { Transactions } from "../../components/Transactions/index.js"; @@ -24,42 +24,375 @@ import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useSettings } from "../../hooks/settings.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import { undefinedIfEmpty } from "../../utils.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { QR } from "../../components/QR.js"; -export function InvalidPaytoView({ error }: State.InvalidPayto) { +export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Payto from server is not valid "{payto}"
    ); } -export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) { +export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Withdrawal uri from server is not valid "{uri}"
    ); } -export function InvalidReserveView({ error }: State.InvalidReserve) { +export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    Reserve from server is not valid "{reserve}"
    ); } -export function NeedConfirmationView({ error }: State.NeedConfirmation) { +export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) { + const { i18n } = useTranslationContext() + + const captchaNumbers = useMemo(() => { + return { + a: Math.floor(Math.random() * 10), + b: Math.floor(Math.random() * 10), + }; + }, []); + const [captchaAnswer, setCaptchaAnswer] = useState(); + const answer = parseInt(captchaAnswer ?? "", 10); + const errors = undefinedIfEmpty({ + answer: !captchaAnswer + ? i18n.str`Answer the question before continue` + : Number.isNaN(answer) + ? i18n.str`The answer should be a number` + : answer !== captchaNumbers.a + captchaNumbers.b + ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` + : undefined, + }) ?? (busy ? {} as Record : undefined); + return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    +
    +

    + Confirm the withdrawal operation +

    +
    +
    + + + + + + + +
    +
    +
    + +
    { + e.preventDefault() + }} + > +
    + +
    +
    + { + setCaptchaAnswer(e.currentTarget.value) + }} + /> +
    + +
    +
    +
    + + +
    + +
    +
    +
    + {/*
    +
    +

    Wire transfer details

    +
    +
    +
    + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.iban}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.account}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + default: + return
    +
    Exchange account
    +
    {details.account.targetPath}
    +
    + + } + })()} +
    +
    Withdrawal identification
    +
    {details.reserve}
    +
    +
    +
    Amount
    +
    To be added
    + // {/* Amounts.stringifyValue(details.amount) +
    +
    +
    +
    */} + +
    +
    +
    + ); } -export function AbortedView({ error }: State.Aborted) { +export function AbortedView({ error, onClose }: State.Aborted) { return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    +
    aborted
    ); } -export function ConfirmedView({ error }: State.Confirmed) { +export function ConfirmedView({ error, onClose }: State.Confirmed) { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() return ( -
    Payto from server is not valid "{error.data.paytoUri}"
    + + +
    + +
    + +
    +
    + +
    +

    + + The wire transfer to the Taler exchange bank's account is completed, now the + exchange will send the requested amount into your GNU Taler wallet. + +

    +
    +
    +
    +
    +
    + + + Do not show this again + + + +
    +
    +
    + +
    +
    + ); } -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); - return
    + 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. + // WebExtension will be using + // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated + document.title = `${document.title} ${uri.withdrawalOperationId}`; + }, []); + const talerWithdrawUri = stringifyWithdrawUri(uri); + const [show, setShow] = useState(false) + return + +
    +
    +

    + On this device +

    +
    +
    +

    + If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled. +

    +
    + +
    +
    +
    +
    +
    +

    + On a mobile phone +

    +
    +
    +

    + Scan the QR code with your mobile device. +

    +
    +
    + +
    +
    + {show && +
    + +
    + } +
    +
    + +
    + +
    +
    } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 573f8c769..2830f5c1e 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -26,12 +26,11 @@ import { useSettings } from "../hooks/settings.js"; * Let the user choose a payment option, * then specify the details trigger the action. */ -export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { +export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = useSettings(); + const [settings] = useSettings(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); - // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); return (
    @@ -56,6 +55,14 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { Withdraw digital money into your mobile wallet or browser extension + {!!settings.currentWithdrawalOperationId && + + + Operation in progress + + }
    -
    -

    - If you have a Taler wallet installed in this device -

    - -
    -

    - You will see the details of the operation in your wallet including the fees (if applies). - If you still one you can install it from here. -

    -
    -
    -
    - -
    -
    -
    - -
    -
    -

    - Or if you have the wallet in another device -

    -
    - Scan the QR below to start the withdrawal -
    -
    - -
    -
    -
    - -
    -
    - - - ); -} - diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 08f706919..8dbdd9da6 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -36,33 +36,52 @@ import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { Amount } from "./PaytoWireTransferForm.js"; import { useSettings } from "../hooks/settings.js"; -import { WithdrawalOperationState } from "./WithdrawalQRCode.js"; -import { Loading } from "../components/Loading.js"; +import { OperationState } from "./OperationState/index.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); -export function WalletWithdrawForm({ - focus, - limit, - onSuccess, - onCancel, -}: { + +function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { limit: AmountJson; focus?: boolean; - onSuccess: (operationId: string) => void; + goToConfirmOperation: (operationId: string) => void; onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); - const { createWithdrawal } = useAccessAPI(); const [settings, updateSettings] = useSettings() + const { createWithdrawal } = useAccessAPI(); const [amountStr, setAmountStr] = useState(`${settings.maxWithdrawalAmount}`); const ref = useRef(null); useEffect(() => { if (focus) ref.current?.focus(); }, [focus]); + if (!!settings.currentWithdrawalOperationId) { + return
    +
    +
    + +
    +
    +

    + There is an operation already +

    +
    +

    + + To complete or cancel the operation click here + +

    +
    +
    +
    +
    + } + const trimmedAmountStr = amountStr?.trim(); const parsedAmount = trimmedAmountStr @@ -92,7 +111,8 @@ export function WalletWithdrawForm({ i18n.str`Server responded with an invalid withdraw URI`, i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`); } else { - onSuccess(uri.withdrawalOperationId); + updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + goToConfirmOperation(uri.withdrawalOperationId); } } catch (error) { if (error instanceof RequestError) { @@ -115,113 +135,168 @@ export function WalletWithdrawForm({ } } + return
    { + e.preventDefault() + }} + > +
    +
    +
    + + { + setAmountStr(v); + }} + error={errors?.amount} + ref={ref} + /> +
    +
    + + + + + + +
    + +
    +
    +
    + + +
    + +
    +} + + +export function WalletWithdrawForm({ + focus, + limit, + onCancel, + goToConfirmOperation, +}: { + limit: AmountJson; + focus?: boolean; + goToConfirmOperation: (operationId: string) => void; + onCancel: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() + return (

    Prepare your wallet

    - Upon starting you will receive the money in your digital wallet, if you don't have one please install one from here. -

    -

    - After using your wallet you will be redirected here to confirm or cancel the operation. + After using your wallet you will confirm or cancel the operation.

    - {!settings.fastWithdrawal ? -
    { - e.preventDefault() - }} - > -
    -
    -
    - - { - setAmountStr(v); - }} - error={errors?.amount} - ref={ref} - /> -
    -
    - - - - - -
    -
    -
    - - -
    +
    } - - : settings.currentWithdrawalOperationId === undefined ? - : - + : + { - onCancel() - }} + onClose={onCancel} + goToConfirmOperation={goToConfirmOperation} /> - } + } +
    ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 9976babdb..25c571e28 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -18,23 +18,17 @@ import { Amounts, HttpStatusCode, Logger, - TranslatedString, WithdrawUriResult, - parsePaytoUri, - parseWithdrawUri, - stringifyWithdrawUri, + parsePaytoUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -import { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js"; +import { useWithdrawalDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; -import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js"; +import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; -import { useEffect, useState } from "preact/hooks"; -import { buildRequestErrorMessage } from "../utils.js"; -import { getInitialBackendBaseURL } from "../hooks/backend.js"; const logger = new Logger("WithdrawalQRCode"); @@ -54,18 +48,11 @@ export function WithdrawalQRCode({ const [settings, updateSettings] = useSettings(); const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); + if (!result.ok) { if (result.loading) { return ; } - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) { - onClose() - return
    operation not found
    ; - } - // onLoadNotOk(); return handleNotOkResult(i18n)(result); } const { data } = result; @@ -127,22 +114,6 @@ export function WithdrawalQRCode({
    -
    -
    - - - Do not show this again - - - -
    -
    +
    @@ -360,39 +370,13 @@ export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { Scan the QR code with your mobile device.

    -
    - -
    - {show && -
    - -
    - } +
    + +
    -
    - -
    } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 2830f5c1e..49419d0dc 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -56,11 +56,11 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ Withdraw digital money into your mobile wallet or browser extension {!!settings.currentWithdrawalOperationId && - + - Operation in progress + operation ready } diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 8221457bf..5325f43ab 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -33,8 +33,10 @@ const logger = new Logger("RegistrationPage"); export function RegistrationPage({ onComplete, + onCancel }: { onComplete: () => void; + onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); if (!bankUiSettings.allowRegistrations) { @@ -42,7 +44,7 @@ export function RegistrationPage({

    {i18n.str`Currently, the bank is not accepting new registrations!`}

    ); } - return ; + return ; } export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/; @@ -50,7 +52,7 @@ export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/; /** * Collect and submit registration data. */ -function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode { +function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode { const backend = useBackendContext(); const [username, setUsername] = useState(); const [name, setName] = useState(); @@ -168,9 +170,38 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode { autoCapitalize="none" autoCorrect="off" > +
    + +
    + { + setName(e.currentTarget.value); + }} + /> + +
    +
    +
    void }): VNode {
    - +
    void }): VNode {
    - +
    void }): VNode {
    -
    +
    +
    diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts index 4399dbcf2..07a402413 100644 --- a/packages/taler-util/src/errors.ts +++ b/packages/taler-util/src/errors.ts @@ -78,7 +78,7 @@ export interface DetailsMap { stack?: string; }; [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: { - exchangeProtocolVersion: string; + bankProtocolVersion: string; walletProtocolVersion: string; }; [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: { diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index fb503d75f..2c9c95d4c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -570,7 +570,7 @@ export async function getBankWithdrawalInfo( throw TalerError.fromDetail( TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE, { - exchangeProtocolVersion: config.version, + bankProtocolVersion: config.version, walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, }, "bank integration protocol version not compatible with wallet", @@ -813,10 +813,10 @@ async function handleKycRequired( amlStatus === AmlStatus.normal || amlStatus === undefined ? WithdrawalGroupStatus.PendingKyc : amlStatus === AmlStatus.pending - ? WithdrawalGroupStatus.PendingAml - : amlStatus === AmlStatus.fronzen - ? WithdrawalGroupStatus.SuspendedAml - : assertUnreachable(amlStatus); + ? WithdrawalGroupStatus.PendingAml + : amlStatus === AmlStatus.fronzen + ? WithdrawalGroupStatus.SuspendedAml + : assertUnreachable(amlStatus); await tx.withdrawalGroups.put(wg2); const newTxState = computeWithdrawalTransactionStatus(wg2); @@ -1145,8 +1145,7 @@ export async function updateWithdrawalDenoms( denom.verificationStatus === DenominationVerificationStatus.Unverified ) { logger.trace( - `Validating denomination (${current + 1}/${ - denominations.length + `Validating denomination (${current + 1}/${denominations.length }) signature of ${denom.denomPubHash}`, ); let valid = false; @@ -1240,7 +1239,7 @@ async function queryReserve( if ( resp.status === 404 && result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN + TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN ) { return { ready: false }; } else { @@ -1775,7 +1774,7 @@ export async function getExchangeWithdrawalInfo( ) { logger.warn( `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` + - `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`, + `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`, ); } } diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts index 8b9177bc3..022f4900d 100644 --- a/packages/taler-wallet-core/src/versions.ts +++ b/packages/taler-wallet-core/src/versions.ts @@ -29,7 +29,7 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0"; export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1"; /** - * Protocol version spoken with the merchant. + * Protocol version spoken with the bank. * * Uses libtool's current:revision:age versioning. */ -- cgit v1.2.3 From 1e4f21cc76345f3881ea8e5ea0e94d27d26da609 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 26 Sep 2023 15:18:43 -0300 Subject: lang selector and fix logout --- packages/demobank-ui/package.json | 2 +- .../demobank-ui/src/components/LangSelector.tsx | 78 ++++++------ packages/demobank-ui/src/components/app.tsx | 6 +- packages/demobank-ui/src/context/backend.ts | 4 + packages/demobank-ui/src/declaration.d.ts | 4 +- packages/demobank-ui/src/hooks/backend.ts | 44 +++++-- packages/demobank-ui/src/hooks/circuit.ts | 4 +- packages/demobank-ui/src/hooks/config.ts | 20 +-- .../demobank-ui/src/hooks/useCredentialsChecker.ts | 2 +- packages/demobank-ui/src/pages/BankFrame.tsx | 128 ++++++------------- packages/demobank-ui/src/pages/LoginForm.tsx | 27 ++-- .../demobank-ui/src/pages/OperationState/views.tsx | 17 +-- .../demobank-ui/src/pages/RegistrationPage.tsx | 138 +++++++++++++++------ packages/demobank-ui/src/pages/admin/Account.tsx | 2 +- packages/demobank-ui/src/pages/business/Home.tsx | 2 - packages/demobank-ui/src/settings.ts | 14 +-- 16 files changed, 272 insertions(+), 220 deletions(-) (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx') diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json index 744cb4180..b430ebc24 100644 --- a/packages/demobank-ui/package.json +++ b/packages/demobank-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/demobank-ui", - "version": "0.1.0", + "version": "0.9.3-dev.17", "license": "AGPL-3.0-OR-LATER", "type": "module", "scripts": { diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx index ca4411682..c1d0f64ef 100644 --- a/packages/demobank-ui/src/components/LangSelector.tsx +++ b/packages/demobank-ui/src/components/LangSelector.tsx @@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string { return String(s); } -// FIXME: explain "like py". -export function LangSelectorLikePy(): VNode { +export function LangSelector(): VNode { const [updatingLang, setUpdatingLang] = useState(false); const { lang, changeLanguage } = useTranslationContext(); const [hidden, setHidden] = useState(true); + useEffect(() => { function bodyKeyPress(event: KeyboardEvent) { if (event.code === "Escape") setHidden(true); @@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode { }; }, []); return ( - - { - ev.preventDefault(); - setHidden((h) => !h); - ev.stopPropagation(); - }} - > - {getLangName(lang)} - -
    -
    - - +
    ); } diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index a587c6f1e..f15a9ee6a 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -78,17 +78,17 @@ function VersionCheck({ children }: { children: ComponentChildren }): VNode { if (checked === undefined) { return } - if (typeof checked === "string") { + if (checked.type === "wrong") { return the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}" } - if (checked === true) { + if (checked.type === "ok") { return {children} } return - + } diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/demobank-ui/src/context/backend.ts index b311ddbb0..eae187c6d 100644 --- a/packages/demobank-ui/src/context/backend.ts +++ b/packages/demobank-ui/src/context/backend.ts @@ -34,6 +34,9 @@ const initial: Type = { logOut() { null; }, + expired() { + null; + }, logIn(info) { null; }, @@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({ const value: BackendStateHandler = { state, logIn: () => {}, + expired: () => {}, logOut: () => {}, }; diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index d3d9e02ef..bd7edf033 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -74,7 +74,9 @@ type HashCode = string; type EddsaPublicKey = string; type EddsaSignature = string; type WireTransferIdentifierRawP = string; -type RelativeTime = Duration; +type RelativeTime = { + d_us: number | "forever" +}; type ImageDataUrl = string; interface WithId { diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 3d5bfa360..889618646 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -46,16 +46,18 @@ import { AccessToken } from "./useCredentialsChecker.js"; * Has the information to reach and * authenticate at the bank's backend. */ -export type BackendState = LoggedIn | LoggedOut; +export type BackendState = LoggedIn | LoggedOut | Expired; -export interface BackendCredentials { +interface LoggedIn { + status: "loggedIn"; + isUserAdministrator: boolean; username: string; token: AccessToken; } - -interface LoggedIn extends BackendCredentials { - status: "loggedIn"; +interface Expired { + status: "expired"; isUserAdministrator: boolean; + username: string; } interface LoggedOut { status: "loggedOut"; @@ -69,6 +71,13 @@ export const codecForBackendStateLoggedIn = (): Codec => .property("isUserAdministrator", codecForBoolean()) .build("BackendState.LoggedIn"); +export const codecForBackendStateExpired = (): Codec => + buildCodecForObject() + .property("status", codecForConstString("expired")) + .property("username", codecForString()) + .property("isUserAdministrator", codecForBoolean()) + .build("BackendState.Expired"); + export const codecForBackendStateLoggedOut = (): Codec => buildCodecForObject() .property("status", codecForConstString("loggedOut")) @@ -79,6 +88,7 @@ export const codecForBackendState = (): Codec => .discriminateOn("status") .alternative("loggedIn", codecForBackendStateLoggedIn()) .alternative("loggedOut", codecForBackendStateLoggedOut()) + .alternative("expired", codecForBackendStateExpired()) .build("BackendState"); export function getInitialBackendBaseURL(): string { @@ -94,8 +104,9 @@ export function getInitialBackendBaseURL(): string { "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); result = window.origin + } else { + result = bankUiSettings.backendBaseURL; } - result = bankUiSettings.backendBaseURL; } else { // testing/development path result = overrideUrl @@ -115,7 +126,8 @@ export const defaultState: BackendState = { export interface BackendStateHandler { state: BackendState; logOut(): void; - logIn(info: BackendCredentials): void; + expired(): void; + logIn(info: {username: string, token: AccessToken}): void; } const BACKEND_STATE_KEY = buildStorageKey( @@ -133,12 +145,22 @@ export function useBackendState(): BackendStateHandler { BACKEND_STATE_KEY, defaultState, ); + const mutateAll = useMatchMutate(); return { state, logOut() { update(defaultState); }, + expired() { + if (state.status === "loggedOut") return; + const nextState: BackendState = { + status: "expired", + username: state.username, + isUserAdministrator: state.username === "admin", + }; + update(nextState); + }, logIn(info) { //admin is defined by the username const nextState: BackendState = { @@ -147,6 +169,7 @@ export function useBackendState(): BackendStateHandler { isUserAdministrator: info.username === "admin", }; update(nextState); + mutateAll(/.*/) }, }; } @@ -194,7 +217,7 @@ export function usePublicBackend(): useBackendType { number, ]): Promise> { const delta = -1 * size //descending order - const params = start ? { delta, start } : {delta} + const params = start ? { delta, start } : { delta } return requestHandler(baseUrl, endpoint, { params, }); @@ -262,7 +285,8 @@ export function useAuthenticatedBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const creds = state.status === "loggedIn" ? state.token : undefined; + // FIXME: libeufin returns 400 insteand of 401 if there is no auth token + const creds = state.status === "loggedIn" ? state.token : "secret-token:a"; const baseUrl = getInitialBackendBaseURL(); const request = useCallback( @@ -288,7 +312,7 @@ export function useAuthenticatedBackend(): useBackendType { number, ]): Promise> { const delta = -1 * size //descending order - const params = start ? { delta, start } : {delta} + const params = start ? { delta, start } : { delta } return requestHandler(baseUrl, endpoint, { token: creds, params, diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 4ef80b055..82caafdf2 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -268,7 +268,7 @@ export function useEstimator(): CashoutEstimators { const { state } = useBackendContext(); const { request } = useApiContext(); const creds = - state.status === "loggedOut" + state.status !== "loggedIn" ? undefined : state.token; return { @@ -340,7 +340,7 @@ export function useBusinessAccountFlag(): boolean | undefined { const { state } = useBackendContext(); const { request } = useApiContext(); const creds = - state.status === "loggedOut" + state.status !== "loggedIn" ? undefined : {user: state.username, token: state.token}; diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts index 4cf677d35..bb5134510 100644 --- a/packages/demobank-ui/src/hooks/config.ts +++ b/packages/demobank-ui/src/hooks/config.ts @@ -18,23 +18,29 @@ async function getConfigState( return result.data; } -export function useConfigState(): undefined | true | string | HttpError { - const [checked, setChecked] = useState>() +type Result = undefined + | { type: "ok", result: SandboxBackend.Config } + | { type: "wrong", result: SandboxBackend.Config } + | { type: "error", result: HttpError } + +export function useConfigState(): Result { + const [checked, setChecked] = useState() const { request } = useApiContext(); useEffect(() => { getConfigState(request) - .then((s) => { - const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, s.version) + .then((result) => { + const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version) if (r?.compatible) { - setChecked(true); + setChecked({ type: "ok",result }); } else { - setChecked(s.version) + setChecked({ type: "wrong",result }) } }) .catch((error: unknown) => { if (error instanceof RequestError) { - setChecked(error.cause); + const result = error.cause + setChecked({ type:"error", result }); } }); }, []); diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts index f66a4a7c6..02f4544db 100644 --- a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts +++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts @@ -15,7 +15,7 @@ export function useCredentialsChecker() { scope: "readwrite" as "write", //FIX: different than merchant duration: { // d_us: "forever" //FIX: should return shortest - d_us: 1000 * 60 * 60 * 23 + d_us: 1000 * 1000 * 5 //60 * 60 * 24 * 7 }, refreshable: true, } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 5c43d2c3e..3d09fcec7 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -18,7 +18,7 @@ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringi import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; -import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; +import { LangSelector } from "../components/LangSelector.js"; import { useBackendContext } from "../context/backend.js"; import { useBusinessAccountDetails } from "../hooks/circuit.js"; import { bankUiSettings } from "../settings.js"; @@ -65,12 +65,14 @@ export function BankFrame({ }, [error]) const demo_sites = []; - for (const i in bankUiSettings.demoSites) - demo_sites.push( - - {bankUiSettings.demoSites[i][0]} - , - ); + if (bankUiSettings.demoSites) { + for (const i in bankUiSettings.demoSites) + demo_sites.push( + + {bankUiSettings.demoSites[i][0]} + , + ); + } return (
    @@ -88,14 +90,16 @@ export function BankFrame({ />
    -
    @@ -166,26 +170,29 @@ export function BankFrame({ - Log out - {/* */} + Log out
  • -
  • -
    - Sites -
    -
      - {bankUiSettings.demoSites.map(([name, url]) => { - return
    • - - > - {name} - -
    • - })} -
    +
  • +
  • - + {bankUiSettings.demoSites && +
  • +
    + Sites +
    +
      + {bankUiSettings.demoSites.map(([name, url]) => { + return
    • + + > + {name} + +
    • + })} +
    +
  • + }
    • @@ -291,63 +298,6 @@ export function BankFrame({
      - // - //
      - // - //
      - // {maybeDemoContent( - //

      - // {IS_PUBLIC_ACCOUNT_ENABLED ? ( - // - // This part of the demo shows how a bank that supports Taler - // directly would work. In addition to using your own bank - // account, you can also see the transaction history of some{" "} - // Public Accounts. - // - // ) : ( - // - // This part of the demo shows how a bank that supports Taler - // directly would work. - // - // )} - //

      , - // )} - //
      - //
      - // - //
      - // - // {children} - //
      - //
      ); } @@ -393,7 +343,7 @@ function StatusBanner(): VNode { } {n.message.debug &&
      - {n.message.debug} + {n.message.debug}
      } diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 786399d55..14d261622 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -30,14 +30,32 @@ import { undefinedIfEmpty } from "../utils.js"; */ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { const backend = useBackendContext(); - const currentUser = backend.state.status === "loggedIn" ? backend.state.username : undefined + const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined const [username, setUsername] = useState(currentUser); const [password, setPassword] = useState(); const { i18n } = useTranslationContext(); const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker(); + + /** + * Register form may be shown in the initialization step. + * If this is an error when usgin the app the registration + * callback is not set + */ + const isSessionExpired = !onRegister + + // useEffect(() => { + // if (backend.state.status === "loggedIn") { + // backend.expired() + // } + // },[]) const ref = useRef(null); useEffect(function focusInput() { + //FIXME: show invalidate session and allow relogin + if (isSessionExpired) { + localStorage.removeItem("backend-state"); + window.location.reload() + } ref.current?.focus(); }, []); const [busy, setBusy] = useState>() @@ -124,13 +142,6 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { setBusy(undefined) } - /** - * Register form may be shown in the initialization step. - * If this is an error when usgin the app the registration - * callback is not set - */ - const isSessionExpired = !onRegister - return (
      diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index 681a3b94d..93b3694d7 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,20 +14,15 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; +import { stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { Transactions } from "../../components/Transactions/index.js"; -import { PaymentOptions } from "../PaymentOptions.js"; -import { State } from "./index.js"; -import { CopyButton } from "../../components/CopyButton.js"; -import { bankUiSettings } from "../../settings.js"; -import { useBusinessAccountDetails } from "../../hooks/circuit.js"; -import { useSettings } from "../../hooks/settings.js"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useMemo, useState } from "preact/hooks"; -import { undefinedIfEmpty } from "../../utils.js"; -import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { QR } from "../../components/QR.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useSettings } from "../../hooks/settings.js"; +import { undefinedIfEmpty } from "../../utils.js"; +import { State } from "./index.js"; export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index a2543f977..2e931a144 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -49,6 +49,8 @@ export function RegistrationPage({ } export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/; +export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/; +export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; /** * Collect and submit registration data. @@ -58,21 +60,33 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on const [username, setUsername] = useState(); const [name, setName] = useState(); const [password, setPassword] = useState(); + const [phone, setPhone] = useState(); + const [email, setEmail] = useState(); const [repeatPassword, setRepeatPassword] = useState(); - const {requestNewLoginToken} = useCredentialsChecker() + const { requestNewLoginToken } = useCredentialsChecker() const { register } = useTestingAPI(); const { i18n } = useTranslationContext(); const errors = undefinedIfEmpty({ - name: !name - ? i18n.str`Missing name` - : undefined, + // name: !name + // ? i18n.str`Missing name` + // : undefined, username: !username ? i18n.str`Missing username` : !USERNAME_REGEX.test(username) ? i18n.str`Use letters and numbers only, and start with a lowercase letter` : undefined, + phone: !phone + ? undefined + : !PHONE_REGEX.test(phone) + ? i18n.str`Use letters and numbers only, and start with a lowercase letter` + : undefined, + email: !email + ? undefined + : !EMAIL_REGEX.test(email) + ? i18n.str`Use letters and numbers only, and start with a lowercase letter` + : undefined, password: !password ? i18n.str`Missing password` : undefined, repeatPassword: !repeatPassword ? i18n.str`Missing password` @@ -82,9 +96,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on }); async function doRegistrationStep() { - if (!username || !password || !name) return; + if (!username || !password) return; try { - await register({ name, username, password }); + await register({ name: name ?? "", username, password }); const resp = await requestNewLoginToken(username, password) setUsername(undefined); if (resp.valid) { @@ -167,7 +181,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
      -

      {i18n.str`Sign up!`}

      +

      {i18n.str`Registration form`}

      @@ -178,34 +192,6 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on autoCapitalize="none" autoCorrect="off" > -
      - -
      - { - setName(e.currentTarget.value); - }} - /> - -
      -
      -
      +
      + +
      +} diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx index f83b61234..ee62671ce 100644 --- a/packages/demobank-ui/src/components/ErrorLoading.tsx +++ b/packages/demobank-ui/src/components/ErrorLoading.tsx @@ -17,25 +17,13 @@ import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; export function ErrorLoading({ error }: { error: HttpError }): VNode { const { i18n } = useTranslationContext() - return ( -
      -
      -
      - -
      -
      -

      {error.message}

      -
      -
      -
      -

      Got status "{error.info.status}" on {error.info.url}

      -
      -
      -
      + return ( +

      Got status "{error.info.status}" on {error.info.url}

      +
      ); } diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index f8b2e3113..f92c874f3 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { State } from "./index.js"; import { format, isToday } from "date-fns"; import { Amounts } from "@gnu-taler/taler-util"; +import { useEffect, useRef } from "preact/hooks"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -55,9 +56,9 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {i18n.str`Date`} - {i18n.str`Amount`} - {i18n.str`Counterpart`} - {i18n.str`Subject`} + {i18n.str`Amount`} + {i18n.str`Counterpart`} + {i18n.str`Subject`} @@ -69,22 +70,38 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {txs.map(item => { + const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss") + const amount = + {item.negative ? "-" : ""} + {item.amount ? ( + `${Amounts.stringifyValue(item.amount)} ${item.amount.currency + }` + ) : ( + <{i18n.str`invalid value`}> + )} + return ( -
      {item.when.t_ms === "never" - ? "" - : format(item.when.t_ms, "HH:mm:ss")}
      +
      {time}
      +
      +
      Amount
      +
      + {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( + `${Amounts.stringifyValue(item.amount)}` + ) : ( + <{i18n.str`invalid value`}> + )}
      +
      Counterpart
      +
      + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} +
      +
      - {item.negative ? "-" : ""} - {item.amount ? ( - `${Amounts.stringifyValue(item.amount)} ${item.amount.currency - }` - ) : ( - <{i18n.str`invalid value`}> - )} - {item.counterpart} + class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> + {amount} + + {item.counterpart} {item.subject} ) })} @@ -94,8 +111,8 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode - -
      - - + return { + updateSettings("showDemoDescription", false); + }}> + {IS_PUBLIC_ACCOUNT_ENABLED ? ( + + This part of the demo shows how a bank that supports Taler + directly would work. In addition to using your own bank + account, you can also see the transaction history of some{" "} + Public Accounts. + + ) : ( + + This part of the demo shows how a bank that supports Taler + directly would work. + + )} + } export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 15ef8a036..29334cae4 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,7 +15,7 @@ */ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { NotificationMessage, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { LangSelector } from "../components/LangSelector.js"; @@ -26,6 +26,7 @@ import { useSettings } from "../hooks/settings.js"; import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import logo from "../assets/logo-2021.svg"; import { useAccountDetails } from "../hooks/access.js"; +import { Attention } from "../components/Attention.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -108,7 +109,7 @@ export function BankFrame({ setOpen(!open) }}> - Open main menu + Open settings @@ -227,6 +228,22 @@ export function BankFrame({
    • +
    • +
      + + + Show debug info + + + +
      +
    • @@ -286,10 +303,10 @@ export function BankFrame({ }
      +
      - {children}
      @@ -301,79 +318,46 @@ export function BankFrame({ ); } +function MaybeShowDebugInfo({ info }: { info: any }): VNode { + const [settings] = useSettings() + if (settings.showDebugInfo) { + return
      +    {info}
      +  
      + } + return +} + function StatusBanner(): VNode { const notifs = useNotifications() - return
      { + if (notifs.length === 0) return + return
      { notifs.map(n => { switch (n.message.type) { case "error": - return
      -
      -
      - -
      -
      -

      {n.message.title}

      -
      -
      -

      - -

      -
      -
      + return { + n.remove() + }}> {n.message.description &&
      {n.message.description}
      } + + {/* + show debug info + {n.message.debug &&
      {n.message.debug}
      - } -
      + } */} + case "info": - return
      -
      -
      - -
      -
      -

      {n.message.title}

      - -

      - -

      -
      - -
      -
      + return { + n.remove(); + }} /> } })}
      diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index d945d80d1..95144f086 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -137,8 +137,8 @@ export function handleNotOkResult( const errorData = result.payload; notify({ type: "error", - title: i18n.str`Could not load due to a client error`, - description: errorData?.error?.description as TranslatedString, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`, debug: JSON.stringify(result), }); break; @@ -174,7 +174,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - route("/") + // route("/") return
      error
      ; } return
      ; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 14d261622..3ea94b899 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -23,6 +23,7 @@ import { useBackendContext } from "../context/backend.js"; import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; +import { doAutoFocus } from "./PaytoWireTransferForm.js"; /** @@ -98,8 +99,8 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { }); } else { saveError({ - title: i18n.str`Could not load due to a client error`, - // description: cause.payload.error.description, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`, debug: JSON.stringify(cause.payload), }); } @@ -159,8 +160,7 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {

      - The wire transfer to the Taler exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. + The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.

      diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 49419d0dc..fef272831 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ const { i18n } = useTranslationContext(); const [settings] = useSettings(); - const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); + const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer"); return (
      @@ -82,7 +82,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ another bank account - Make a wire transfer to an account which you know the address. + Make a wire transfer to an account which you know the bank account number @@ -108,6 +108,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ limit={limit} onSuccess={() => { notifyInfo(i18n.str`Wire transfer created!`); + setTab(undefined) }} onCancel={() => { setTab(undefined) diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 5f5a6ce3b..785dc4264 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -55,10 +55,11 @@ export function PaytoWireTransferForm({ onCancel: (() => void) | undefined; limit: AmountJson; }): VNode { - const [isRawPayto, setIsRawPayto] = useState(false); - const [iban, setIban] = useState(undefined); - const [subject, setSubject] = useState(undefined); - const [amount, setAmount] = useState(undefined); + const [isRawPayto, setIsRawPayto] = useState(true); + // FIXME: remove this + const [iban, setIban] = useState("DE4745461198061"); + const [subject, setSubject] = useState("ASD"); + const [amount, setAmount] = useState("1.00001"); const [rawPaytoInput, rawPaytoInputSetter] = useState( undefined, @@ -76,17 +77,17 @@ export function PaytoWireTransferForm({ const errorsWire = undefinedIfEmpty({ iban: !iban - ? i18n.str`Missing IBAN` + ? i18n.str`required` : !IBAN_REGEX.test(iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(iban, i18n), - subject: !subject ? i18n.str`Missing subject` : undefined, + subject: !subject ? i18n.str`required` : undefined, amount: !trimmedAmountStr - ? i18n.str`Missing amount` + ? i18n.str`required` : !parsedAmount - ? i18n.str`Amount is not valid` + ? i18n.str`not valid` : Amounts.isZero(parsedAmount) - ? i18n.str`Should be greater than 0` + ? i18n.str`should be greater than 0` : Amounts.cmp(limit, parsedAmount) === -1 ? i18n.str`balance is not enough` : undefined, @@ -101,14 +102,14 @@ export function PaytoWireTransferForm({ ? i18n.str`required` : !parsed ? i18n.str`does not follow the pattern` - : !parsed.params.amount - ? i18n.str`use the "amount" parameter to specify the amount to be transferred` - : Amounts.parse(parsed.params.amount) === undefined - ? i18n.str`the amount is not valid` - : !parsed.params.message - ? i18n.str`use the "message" parameter to specify a reference text for the transfer` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`only "IBAN" target are supported` + : !parsed.isKnown || parsed.targetType !== "iban" + ? i18n.str`only "IBAN" target are supported` + : !parsed.params.amount + ? i18n.str`use the "amount" parameter to specify the amount to be transferred` + : Amounts.parse(parsed.params.amount) === undefined + ? i18n.str`the amount is not valid` + : !parsed.params.message + ? i18n.str`use the "message" parameter to specify a reference text for the transfer` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(parsed.iban, i18n), @@ -159,6 +160,9 @@ export function PaytoWireTransferForm({ } return (
      + {/** + * FIXME: Scan a qr code + */}

      {title} @@ -167,6 +171,17 @@ export function PaytoWireTransferForm({
      { @@ -203,105 +228,106 @@ export function PaytoWireTransferForm({ }} >
      -
      - {!isRawPayto ? - - -
      - -
      - { - setIban(e.currentTarget.value); - }} - /> - -
      -

      the receiver of the money

      -
      + {!isRawPayto ? +
      -
      - -
      - { - setSubject(e.currentTarget.value); - }} - /> - -
      -

      some text to identify the transfer

      +
      + +
      + { + setIban(e.currentTarget.value.toUpperCase()); + }} + /> +
      +

      + IBAN of the recipient's account +

      +
      -
      - - { - setAmount(d) +
      + +
      + { + setSubject(e.currentTarget.value); }} /> -

      amount to transfer

      +

      some text to identify the transfer

      +
      - : - -
      - -
      - { - rawPaytoInputSetter(e.currentTarget.value); - }} - /> - -
      -
      +
      + + { + setAmount(d) + }} + /> + +

      amount to transfer

      +
      -
      - } -
      +
      : +
      +
      + +
      +