aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-21 20:25:38 -0300
committerSebastian <sebasjm@gmail.com>2023-10-21 20:25:50 -0300
commit2ac73949e7cb8de44e56f2fecae617efab15671e (patch)
tree144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui/src/pages
parent4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff)
downloadwallet-core-2ac73949e7cb8de44e56f2fecae617efab15671e.tar.xz
more ui
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx29
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx122
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx12
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts15
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx24
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx65
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx56
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx16
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx123
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx259
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx1
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx10
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx182
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx58
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx32
-rw-r--r--packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx47
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx24
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx143
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx3
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx (renamed from packages/demobank-ui/src/pages/business/Home.tsx)346
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx237
21 files changed, 921 insertions, 883 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index 0604001e3..00643ec3e 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -54,40 +54,11 @@ function ShowDemoInfo(): VNode {
}
export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
- const { i18n } = useTranslationContext();
return <Fragment>
- <MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
-
<ShowDemoInfo />
-
<PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
<Transactions account={account} />
</Fragment>;
}
-function MaybeBusinessButton({
- account,
- onClick,
-}: {
- account: string;
- onClick: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- return <Fragment />
- // const result = useBusinessAccountDetails(account);
- // if (!result.ok) return <Fragment />;
- // return (
- // <div class="w-full flex justify-end">
- // <button
- // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- // onClick={(e) => {
- // e.preventDefault()
- // onClick()
- // }}
- // >
- // <i18n.Translate>Business Profile</i18n.Translate>
- // </button>
- // </div>
- // );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 96ce9c317..c0babd0c9 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -23,8 +23,8 @@ import { Attention } from "../components/Attention.js";
import { CopyButton } from "../components/CopyButton.js";
import { LangSelector } from "../components/LangSelector.js";
import { Loading } from "../components/Loading.js";
-import { useBackendContext } from "../context/backend.js";
import { useAccountDetails } from "../hooks/access.js";
+import { useBackendState } from "../hooks/backend.js";
import { getAllBooleanSettings, getLabelForSetting, useSettings } from "../hooks/settings.js";
import { bankUiSettings } from "../settings.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
@@ -49,7 +49,7 @@ export function BankFrame({
children: ComponentChildren;
}): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const backend = useBackendState();
const [settings, updateSettings] = useSettings();
const [open, setOpen] = useState(false)
@@ -80,9 +80,9 @@ export function BankFrame({
return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
<div class="bg-indigo-600 pb-32">
<nav class="">
- <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
+ <div class="mx-auto max-w-7xl px-2 ">
<div class="relative flex h-16 items-center justify-between ">
- <div class="flex items-center px-2 lg:px-0">
+ <div class="flex items-center px-2">
<div class="flex-shrink-0 bg-white rounded-lg">
<a href={bankUiSettings.iconLinkURL ?? "#"}>
<img
@@ -94,7 +94,7 @@ export function BankFrame({
</a>
</div>
{bankUiSettings.demoSites &&
- <div class="hidden sm:block lg:ml-10 ">
+ <div class="hidden sm:block ml-6 ">
<div class="flex space-x-4">
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
{bankUiSettings.demoSites.map(([name, url]) => {
@@ -161,41 +161,26 @@ export function BankFrame({
<div class="relative mt-6 flex-1 px-4 sm:px-6">
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
- <li>
- <a href="#"
- class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
- onClick={() => {
- backend.logOut();
- setOpen(false)
- updateSettings("currentWithdrawalOperationId", undefined);
- }}
- >
- <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
- </svg>
- <i18n.Translate>Log out</i18n.Translate>
- </a>
- </li>
+ {backend.state.status === "loggedIn" ?
+ <li>
+ <a href="#"
+ class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ onClick={() => {
+ backend.logOut();
+ setOpen(false)
+ updateSettings("currentWithdrawalOperationId", undefined);
+ }}
+ >
+ <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+ </svg>
+ <i18n.Translate>Log out</i18n.Translate>
+ </a>
+ </li>
+ : undefined}
<li>
<LangSelector />
</li>
- {bankUiSettings.demoSites &&
- <li class="sm:hidden">
- <div class="text-xs font-semibold leading-6 text-gray-400">
- <i18n.Translate>Sites</i18n.Translate>
- </div>
- <ul role="list" class="space-y-1">
- {bankUiSettings.demoSites.map(([name, url]) => {
- return <li>
- <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
- <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
- <span class="truncate">{name}</span>
- </a>
- </li>
- })}
- </ul>
- </li>
- }
<li>
<div class="text-xs font-semibold leading-6 text-gray-400">
<i18n.Translate>Preferences</i18n.Translate>
@@ -220,6 +205,23 @@ export function BankFrame({
})}
</ul>
</li>
+ {bankUiSettings.demoSites &&
+ <li class="sm:hidden">
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Sites</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <li>
+ <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
+ <span class="truncate">{name}</span>
+ </a>
+ </li>
+ })}
+ </ul>
+ </li>
+ }
</ul>
</nav>
</div>
@@ -342,28 +344,30 @@ function Footer() {
function WelcomeAccount({ account: accountName }: { account: string }): VNode {
const { i18n } = useTranslationContext();
-
- const result = useAccountDetails(accountName);
- if (!result) {
- return <Loading />
- }
- if (result instanceof TalerError) {
- return <div />
- }
-
- const payto = result.type === "fail" ? undefined : parsePaytoUri(result.body.payto_uri)
- const info = !payto || !payto.isKnown ? undefined
- : payto.targetType === "iban" ? { account: payto.iban, uri: stringifyPaytoUri(payto) }
- : payto.targetType === "x-taler-bank" ? { account: payto.account, uri: stringifyPaytoUri(payto) }
- : undefined;
-
- return <i18n.Translate>
- Welcome, <span class="whitespace-nowrap">{accountName}</span> {info !== undefined ?
- <small class="whitespace-nowrap">
- (<a href={info.uri}>{info.account}</a> <CopyButton getContent={() => info.uri} />)
- </small>
- : <Fragment />}!
- </i18n.Translate>
+ return <a href="#/my-profile" class="underline underline-offset-2">
+ <i18n.Translate>Welcome, <span class="whitespace-nowrap">{accountName}</span></i18n.Translate>
+ </a>
+ // const result = useAccountDetails(accountName);
+ // if (!result) {
+ // return <Loading />
+ // }
+ // if (result instanceof TalerError) {
+ // return <div />
+ // }
+
+ // const payto = result.type === "fail" ? undefined : parsePaytoUri(result.body.payto_uri)
+ // const info = !payto || !payto.isKnown ? undefined
+ // : payto.targetType === "iban" ? { account: payto.iban, uri: stringifyPaytoUri(payto) }
+ // : payto.targetType === "x-taler-bank" ? { account: payto.account, uri: stringifyPaytoUri(payto) }
+ // : undefined;
+
+ // return <i18n.Translate>
+ // Welcome, <span class="whitespace-nowrap">{accountName}</span> {info !== undefined ?
+ // <small class="whitespace-nowrap">
+ // (<a href={info.uri}>{info.account}</a> <CopyButton getContent={() => info.uri} />)
+ // </small>
+ // : <Fragment />}!
+ // </i18n.Translate>
}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 981b0f880..6d1d35288 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,24 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TalerAuthentication, TranslatedString } from "@gnu-taler/taler-util";
-import { ErrorType, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { notify, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { useBackendContext } from "../context/backend.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
-import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "./HomePage.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
/**
* Collect and submit login data.
*/
export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forbidden", onRegister?: () => void }): VNode {
- const backend = useBackendContext();
+ const backend = useBackendState();
const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
index c9c1fa238..9e34a846b 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -86,10 +86,10 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
const wid = withdrawalOperationId
async function doAbort() {
- setBusy({})
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.abortWithdrawalById(wid);
if (resp.type === "ok") {
+ updateSettings("currentWithdrawalOperationId", undefined)
onClose();
} else {
switch (resp.case) {
@@ -103,7 +103,6 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
}
}
})
- setBusy(undefined)
}
async function doConfirm() {
@@ -220,11 +219,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
status: "ready",
error: undefined,
uri: parsedUri,
- onClose: async () => {
- await doAbort()
- updateSettings("currentWithdrawalOperationId", undefined)
- onClose()
- },
+ onClose: doAbort,
onAbort: doAbort,
}
}
@@ -252,11 +247,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
return {
status: "need-confirmation",
error: undefined,
- onAbort: async () => {
- await doAbort()
- updateSettings("currentWithdrawalOperationId", undefined)
- onClose()
- },
+ onAbort: doAbort,
busy: !!busy,
onConfirm: doConfirm
}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index c2d87d0e6..d0ee0243d 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -30,8 +30,8 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
const { i18n } = useTranslationContext();
const [settings] = useSettings();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer");
-
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
+ console.log("patment", tab)
return (
<div class="mt-2">
@@ -48,9 +48,9 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
}} />
<div class="flex flex-col">
<span class="flex">
- <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
+ <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
<span class="grow self-center text-lg text-gray-900 align-middle text-center">
- <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
+ <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
</span>
<svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
@@ -59,14 +59,14 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
<div class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
</div>
- {!!settings.currentWithdrawalOperationId &&
- <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
- <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
- <circle cx="3" cy="3" r="3" />
- </svg>
- <i18n.Translate>operation ready</i18n.Translate>
- </span>
- }
+ {!!settings.currentWithdrawalOperationId &&
+ <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>operation ready</i18n.Translate>
+ </span>
+ }
</div>
</label>
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index e713324c5..d859c10d7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -51,20 +51,25 @@ const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
title,
+ toAccount,
onSuccess,
onCancel,
limit,
}: {
title: TranslatedString,
focus?: boolean;
+ toAccount?: string,
onSuccess: () => void;
onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
- const [isRawPayto, setIsRawPayto] = useState(true);
+ const [isRawPayto, setIsRawPayto] = useState(false);
const { state: credentials } = useBackendState()
const { api } = useBankCoreApiContext();
- const [iban, setIban] = useState<string | undefined>();
+
+ const sendingToFixedAccount = toAccount !== undefined
+ //FIXME: support other destination that just IBAN
+ const [iban, setIban] = useState<string | undefined>(toAccount);
const [subject, setSubject] = useState<string | undefined>();
const [amount, setAmount] = useState<string | undefined>();
@@ -163,7 +168,7 @@ export function PaytoWireTransferForm({
setAmount(undefined);
setIban(undefined);
setSubject(undefined);
- rawPaytoInputSetter(undefined)
+ rawPaytoInputSetter(undefined)
})
}
@@ -181,9 +186,12 @@ export function PaytoWireTransferForm({
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => {
if (parsed && parsed.isKnown && parsed.targetType === "iban") {
setIban(parsed.iban)
- const amount = Amounts.parse(parsed.params["amount"])
- if (amount) {
- setAmount(Amounts.stringifyValue(amount))
+ const amountStr = parsed.params["amount"]
+ if (amountStr) {
+ const amount = Amounts.parse(parsed.params["amount"])
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount))
+ }
}
const subject = parsed.params["message"]
if (subject) {
@@ -201,28 +209,30 @@ export function PaytoWireTransferForm({
</span>
</label>
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => {
- if (iban) {
- const payto = buildPayto("iban", iban, undefined)
- if (parsedAmount) {
- payto.params["amount"] = Amounts.stringify(parsedAmount)
- }
- if (subject) {
- payto.params["message"] = subject
+ {sendingToFixedAccount ? undefined :
+ <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
+ <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => {
+ if (iban) {
+ const payto = buildPayto("iban", iban, undefined)
+ if (parsedAmount) {
+ payto.params["amount"] = Amounts.stringify(parsedAmount)
+ }
+ if (subject) {
+ payto.params["message"] = subject
+ }
+ rawPaytoInputSetter(stringifyPaytoUri(payto))
}
- rawPaytoInputSetter(stringifyPaytoUri(payto))
- }
- setIsRawPayto(true)
- }} />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Import payto:// URI</i18n.Translate>
+ setIsRawPayto(true)
+ }} />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Import payto:// URI</i18n.Translate>
+ </span>
</span>
</span>
- </span>
- </label>
+ </label>
+ }
</div>
</div>
</div>
@@ -244,9 +254,10 @@ export function PaytoWireTransferForm({
<input
ref={focus ? doAutoFocus : undefined}
type="text"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="iban"
id="iban"
+ disabled={sendingToFixedAccount}
value={iban ?? ""}
placeholder="CC0123456789"
autocomplete="off"
@@ -369,7 +380,7 @@ export function PaytoWireTransferForm({
export function doAutoFocus(element: HTMLElement | null) {
if (element) {
setTimeout(() => {
- element.focus()
+ element.focus({ preventScroll: true })
element.scrollIntoView({
behavior: "smooth",
block: "center",
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
new file mode 100644
index 000000000..c061c9742
--- /dev/null
+++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
@@ -0,0 +1,56 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useBankCoreApiContext } from "../context/config.js";
+import { assertUnreachable } from "./HomePage.js";
+
+export function ProfileNavigation({ current }: { current: "details" | "credentials" | "cashouts" }): VNode {
+ const { i18n } = useTranslationContext()
+ const { config } = useBankCoreApiContext()
+ return <div>
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only"><i18n.Translate>Select a section</i18n.Translate></label>
+ <select id="tabs" name="tabs" class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" onChange={(e) => {
+ const op = e.currentTarget.value as typeof current
+ switch (op) {
+ case "details": {
+ window.location.href = "#/my-profile";
+ return;
+ }
+ case "credentials": {
+ window.location.href = "#/my-password";
+ return;
+ }
+ case "cashouts": {
+ window.location.href = "#/my-cashouts";
+ return;
+ }
+ default: assertUnreachable(op)
+ }
+ }}>
+ <option value="details" selected={current == "details"}><i18n.Translate>Details</i18n.Translate></option>
+ <option value="credentials" selected={current == "credentials"}><i18n.Translate>Credentials</i18n.Translate></option>
+ {config.have_cashout ?
+ <option value="cashouts" selected={current == "cashouts"}><i18n.Translate>Cashouts</i18n.Translate></option>
+ : undefined}
+ </select>
+ </div>
+ <div class="hidden sm:block">
+ <nav class="isolate flex divide-x divide-gray-200 rounded-lg shadow" aria-label="Tabs">
+ <a href="#/my-profile" data-selected={current == "details"} class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" >
+ <span><i18n.Translate>Details</i18n.Translate></span>
+ <span aria-hidden="true" data-selected={current == "details"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ <a href="#/my-password" data-selected={current == "credentials"} aria-current="page" class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
+ <span><i18n.Translate>Credentials</i18n.Translate></span>
+ <span aria-hidden="true" data-selected={current == "credentials"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ {config.have_cashout ?
+ <a href="#/my-cashouts" data-selected={current == "cashouts"} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
+ <span>Cashouts</span>
+ <span aria-hidden="true" data-selected={current == "cashouts"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ </a>
+ : undefined}
+ </nav>
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index ce38a9fb8..3520405c5 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,21 +13,19 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AccessToken, HttpStatusCode, Logger, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { AccessToken, Logger, TranslatedString } from "@gnu-taler/taler-util";
import {
- RequestError,
notify,
- notifyError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { bankUiSettings } from "../settings.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { getRandomPassword, getRandomUsername } from "./rnd.js";
import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
+import { bankUiSettings } from "../settings.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
const logger = new Logger("RegistrationPage");
@@ -55,7 +53,7 @@ export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
* Collect and submit registration data.
*/
function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
- const backend = useBackendContext();
+ const backend = useBackendState();
const [username, setUsername] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index c65b90503..21724474a 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -1,25 +1,24 @@
-import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoading } from "../components/ErrorLoading.js";
import { Loading } from "../components/Loading.js";
import { useBankCoreApiContext } from "../context/config.js";
import { useAccountDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
import { assertUnreachable } from "./HomePage.js";
import { LoginForm } from "./LoginForm.js";
import { AccountForm } from "./admin/AccountForm.js";
+import { ProfileNavigation } from "./ProfileNavigation.js";
export function ShowAccountDetails({
account,
onClear,
onUpdateSuccess,
- onChangePassword,
}: {
onClear?: () => void;
- onChangePassword: () => void;
onUpdateSuccess: () => void;
account: string;
}): VNode {
@@ -27,6 +26,8 @@ export function ShowAccountDetails({
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api } = useBankCoreApiContext()
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === account : false
const [update, setUpdate] = useState(false);
const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>();
@@ -47,11 +48,7 @@ export function ShowAccountDetails({
}
async function doUpdate() {
- if (!update) {
- setUpdate(true);
- return;
- }
- if (!submitAccount || !creds) return;
+ if (!update || !submitAccount || !creds) return;
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.updateAccount(creds, {
cashout_address: submitAccount.cashout_payto_uri,
@@ -62,8 +59,9 @@ export function ShowAccountDetails({
is_exchange: false,
name: submitAccount.name,
});
-
+
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account updated`);
onUpdateSuccess();
} else {
switch (resp.case) {
@@ -87,21 +85,23 @@ export function ShowAccountDetails({
}
return (
- <div>
+ <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="details" />
+ :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{account}"</i18n.Translate>
+ </h1>
+
+ }
+
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
- {update ?
- <i18n.Translate>Update account</i18n.Translate>
- :
- <i18n.Translate>Account details</i18n.Translate>
- }
- </h2>
- <div class="mt-4">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- <i18n.Translate>change the account details</i18n.Translate>
+ <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <i18n.Translate>Change details</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
@@ -111,69 +111,36 @@ export function ShowAccountDetails({
<span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
- </div>
-
+ </h2>
</div>
+
<AccountForm
+ focus={update}
+ username={account}
template={result.body}
purpose={update ? "update" : "show"}
onChange={(a) => setSubmitAccount(a)}
>
-
- </AccountForm>
-
- <p class="buttons-account">
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- flexFlow: "wrap-reverse",
- }}
- >
- <div>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
- </div>
- <div style={{ display: "flex" }}>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={i18n.str`Change password`}
- onClick={async (e) => {
- e.preventDefault();
- onChangePassword();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={update ? i18n.str`Confirm` : i18n.str`Update`}
- onClick={async (e) => {
- e.preventDefault();
- doUpdate()
- }}
- />
- </div>
- </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ {onClear ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onClear}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!update || !submitAccount}
+ onClick={doUpdate}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
</div>
- </p>
+ </AccountForm>
</div>
- </div>
+ </Fragment>
);
}
+
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index d82dac4b1..e3f0de8cc 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -1,16 +1,16 @@
-import { TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
import { useBankCoreApiContext } from "../context/config.js";
-import { assertUnreachable } from "./HomePage.js";
import { useBackendState } from "../hooks/backend.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { assertUnreachable } from "./HomePage.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { ProfileNavigation } from "./ProfileNavigation.js";
export function UpdateAccountPassword({
- account,
+ account: accountName,
onCancel,
onUpdateSuccess,
focus,
@@ -22,13 +22,18 @@ export function UpdateAccountPassword({
}): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
+ const token = credentials.status !== "loggedIn" ? undefined : credentials.token
const { api } = useBankCoreApiContext();
+ const [current, setCurrent] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>();
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === accountName : false
+
const errors = undefinedIfEmpty({
+ current: !accountIsTheCurrentUser ? undefined : !current ? i18n.str`required` : undefined,
password: !password ? i18n.str`required` : undefined,
repeat: !repeat
? i18n.str`required`
@@ -37,13 +42,16 @@ export function UpdateAccountPassword({
: undefined,
});
+
async function doChangePassword() {
- if (!!errors || !password || !creds) return;
+ if (!!errors || !password || !token) return;
await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.updatePassword(creds, {
+ const resp = await api.updatePassword({ username: accountName, token }, {
+ // old_password: current,
new_password: password,
});
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Password changed`);
onUpdateSuccess();
} else {
switch (resp.case) {
@@ -51,6 +59,10 @@ export function UpdateAccountPassword({
type: "error",
title: i18n.str`Not authorized to change the password, maybe the session is invalid.`
})
+ case "no-rights": return notify({
+ type: "error",
+ title: i18n.str`This user have no right on to change the password.`
+ })
case "not-found": return notify({
type: "error",
title: i18n.str`Account not found`
@@ -62,112 +74,147 @@ export function UpdateAccountPassword({
}
return (
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Update password for account "{account}"</i18n.Translate>
- </h2>
- </div>
- <form
- 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()
- }}
- >
- <div class="px-4 py-6 sm:p-8">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="credentials" /> :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{accountName}"</i18n.Translate>
+ </h1>
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`New password`}
- </label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- type="password"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="password"
- id="password"
- data-error={!!errors?.password && password !== undefined}
- value={password ?? ""}
- onChange={(e) => {
- setPassword(e.currentTarget.value)
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </div>
- {/* <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>user </i18n.Translate>
- </p> */}
- </div>
+ }
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="repeat"
- >
- {i18n.str`Type it again`}
- </label>
- <div class="mt-2">
- <input
- type="password"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="repeat"
- id="repeat"
- data-error={!!errors?.repeat && repeat !== undefined}
- value={repeat ?? ""}
- onChange={(e) => {
- setRepeat(e.currentTarget.value)
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.repeat}
- isDirty={repeat !== undefined}
- />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Update password</i18n.Translate>
+ </h2>
+ </div>
+ <form
+ 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()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`New password`}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="password"
+ id="password"
+ data-error={!!errors?.password && password !== undefined}
+ value={password ?? ""}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value)
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>repeat the same password</i18n.Translate>
- </p>
- </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="repeat"
+ >
+ {i18n.str`Type it again`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="repeat"
+ id="repeat"
+ data-error={!!errors?.repeat && repeat !== undefined}
+ value={repeat ?? ""}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value)
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>repeat the same password</i18n.Translate>
+ </p>
+ </div>
+ {accountIsTheCurrentUser ?
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="password"
+ >
+ {i18n.str`Current password`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="current"
+ id="current-password"
+ data-error={!!errors?.current && current !== undefined}
+ value={current ?? ""}
+ onChange={(e) => {
+ setCurrent(e.currentTarget.value)
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.current}
+ isDirty={current !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>your current password, for security</i18n.Translate>
+ </p>
+ </div>
+ : undefined}
+ </div>
</div>
- </div>
- <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!!errors}
+ onClick={(e) => {
+ e.preventDefault()
+ doChangePassword()
+ }}
>
- <i18n.Translate>Cancel</i18n.Translate>
+ <i18n.Translate>Change</i18n.Translate>
</button>
- : <div />
- }
- <button type="submit"
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- disabled={!!errors}
- onClick={(e) => {
- e.preventDefault()
- doChangePassword()
- }}
- >
- <i18n.Translate>Change</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
+ </div>
+ </form>
+ </div>
+ </Fragment>
);
} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 7266e4de4..51edbc95f 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -30,6 +30,7 @@ import { assertUnreachable } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
import { Attention } from "../components/Attention.js";
+import { Pages } from "../pages.js";
const logger = new Logger("WithdrawalQRCode");
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx
index bf2fa86f0..103747414 100644
--- a/packages/demobank-ui/src/pages/admin/Account.tsx
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -3,15 +3,15 @@ import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
-import { useBackendContext } from "../../context/backend.js";
import { useAccountDetails } from "../../hooks/access.js";
import { assertUnreachable } from "../HomePage.js";
import { LoginForm } from "../LoginForm.js";
import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js";
+import { useBackendState } from "../../hooks/backend.js";
-export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
+export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode {
const { i18n } = useTranslationContext();
- const r = useBackendContext();
+ const r = useBackendState();
const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
const result = useAccountDetails(account);
@@ -41,11 +41,13 @@ export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode
return (
<PaytoWireTransferForm
title={i18n.str`Make a wire transfer`}
+ toAccount={toAccount}
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
+ if (onSuccess) onSuccess()
}}
- onCancel={undefined}
+ onCancel={onCancel}
/>
);
}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 8470930bf..bce089560 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,16 +1,20 @@
-import { ComponentChildren, VNode, h } from "preact";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
import { useEffect, useRef, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
+import { CopyButton } from "../../components/CopyButton.js";
+import { assertUnreachable } from "../HomePage.js";
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+export type AccountFormData = TalerCorebankApi.AccountData & { username: string }
+
/**
* Create valid account object to update or create
* Take template as initial values for the form
@@ -21,6 +25,7 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
*/
export function AccountForm({
template,
+ username,
purpose,
onChange,
focus,
@@ -28,11 +33,12 @@ export function AccountForm({
}: {
focus?: boolean,
children: ComponentChildren,
+ username?: string,
template: TalerCorebankApi.AccountData | undefined;
- onChange: (a: TalerCorebankApi.AccountData | undefined) => void;
+ onChange: (a: AccountFormData | undefined) => void;
purpose: "create" | "update" | "show";
}): VNode {
- const initial = initializeFromTemplate(template);
+ const initial = initializeFromTemplate(username, template);
const [form, setForm] = useState(initial);
const [errors, setErrors] = useState<
RecursivePartial<typeof initial> | undefined
@@ -69,14 +75,8 @@ export function AccountForm({
? i18n.str`phone number can't have other than numbers`
: undefined,
}),
- // iban: !newForm.iban
- // ? undefined //optional field
- // : !IBAN_REGEX.test(newForm.iban)
- // ? i18n.str`IBAN should have just uppercased letters and numbers`
- // : validateIBAN(newForm.iban, i18n),
name: !newForm.name ? i18n.str`required` : undefined,
-
- // username: !newForm.username ? i18n.str`required` : undefined,
+ username: !newForm.username ? i18n.str`required` : undefined,
});
setErrors(errors);
setForm(newForm);
@@ -95,7 +95,7 @@ export function AccountForm({
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- {/* <div class="sm:col-span-5">
+ <div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="username"
@@ -105,7 +105,7 @@ export function AccountForm({
</label>
<div class="mt-2">
<input
- ref={focus ? doAutoFocus : undefined}
+ ref={focus && purpose === "create" ? doAutoFocus : undefined}
type="text"
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="username"
@@ -128,7 +128,7 @@ export function AccountForm({
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>account identification in the bank</i18n.Translate>
</p>
- </div> */}
+ </div>
<div class="sm:col-span-5">
<label
@@ -165,27 +165,7 @@ export function AccountForm({
</div>
- {purpose !== "create" && (<div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="internal-iban"
- >
- {i18n.str`Internal IBAN`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="internal-iban"
- id="internal-iban"
- disabled={true}
- value={form.payto_uri ?? ""}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>international bank account number</i18n.Translate>
- </p>
- </div>)}
+ {purpose !== "create" && (<RenderPaytoDisabledField paytoURI={form.payto_uri} />)}
<div class="sm:col-span-5">
<label
@@ -264,6 +244,7 @@ export function AccountForm({
<div class="mt-2">
<input
type="text"
+ ref={focus && purpose === "update" ? doAutoFocus : undefined}
data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="cashout"
@@ -294,8 +275,9 @@ export function AccountForm({
}
function initializeFromTemplate(
+ username: string | undefined,
account: TalerCorebankApi.AccountData | undefined,
-): WithIntermediate<TalerCorebankApi.AccountData> {
+): WithIntermediate<AccountFormData> {
const emptyAccount = {
cashout_payto_uri: undefined,
contact_data: undefined,
@@ -314,8 +296,136 @@ function initializeFromTemplate(
if (typeof initial.contact_data === "undefined") {
initial.contact_data = emptyContact;
}
- // initial.contact_data.email;
+ const result: WithIntermediate<AccountFormData> = initial as any // FIXME: check types
+ result.username = username
+
return initial as any;
}
+function RenderPaytoDisabledField({ paytoURI }: { paytoURI: string | undefined }): VNode {
+ const { i18n } = useTranslationContext()
+ const payto = parsePaytoUri(paytoURI ?? "");
+ if (payto?.isKnown) {
+ if (payto.targetType === "iban") {
+ const value = payto.iban;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="internal-iban"
+ >
+ {i18n.str`Internal IBAN`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="internal-iban"
+ id="internal-iban"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>international bank account number</i18n.Translate>
+ </p>
+ </div>
+ }
+ if (payto.targetType === "x-taler-bank") {
+ const value = payto.account;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-id"
+ >
+ {i18n.str`Account ID`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-id"
+ id="account-id"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>internal account id</i18n.Translate>
+ </p>
+ </div>
+ }
+ if (payto.targetType === "bitcoin") {
+ const value = payto.targetPath;
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-id"
+ >
+ {i18n.str`Bitcoin address`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-id"
+ id="account-id"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>bitcoin address</i18n.Translate>
+ </p>
+ </div>
+ }
+ assertUnreachable(payto)
+ }
+
+ const value = paytoURI ?? ""
+ return <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="internal-payto"
+ >
+ {i18n.str`Internal account`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="internal-payto"
+ id="internal-payto"
+ disabled={true}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>generic payto URI</i18n.Translate>
+ </p>
+ </div>
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 8a1e8294a..39b43b9b1 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -1,22 +1,26 @@
import { Amounts, TalerError } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
import { useBusinessAccounts } from "../../hooks/circuit.js";
import { assertUnreachable } from "../HomePage.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { AccountAction } from "./Home.js";
+import { useBankCoreApiContext } from "../../context/config.js";
interface Props {
- onAction: (type: AccountAction, account: string) => void;
- account: string | undefined;
onCreateAccount: () => void;
+
+ onShowAccountDetails: (aid: string) => void;
+ onRemoveAccount: (aid: string) => void;
+ onUpdateAccountPassword: (aid: string) => void;
+ onShowCashoutForAccount: (aid: string) => void;
}
-export function AccountList({ account, onAction, onCreateAccount }: Props): VNode {
+export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAccountPassword, onShowCashoutForAccount, onCreateAccount }: Props): VNode {
const result = useBusinessAccounts();
const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext()
if (!result) {
return <Loading />
@@ -74,6 +78,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
const balance = !item.balance
? undefined
: Amounts.parse(item.balance.amount);
+ const noBalance = Amounts.isZero(item.balance.amount)
const balanceIsDebit =
item.balance &&
item.balance.credit_debit_indicator == "debit";
@@ -83,7 +88,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
- onAction("show-details", item.username)
+ onShowAccountDetails(item.username)
}}
>
{item.username}
@@ -94,7 +99,7 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{item.name}
</td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ <td data-negative={noBalance ? undefined : balanceIsDebit ? "true" : "false"} class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 ">
{!balance ? (
i18n.str`unknown`
) : (
@@ -107,27 +112,34 @@ export function AccountList({ account, onAction, onCreateAccount }: Props): VNod
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
- onAction("update-password", item.username)
+ onUpdateAccountPassword(item.username)
}}
>
change password
</a>
<br />
- <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
- e.preventDefault();
- onAction("show-cashout", item.username)
- }}
- >
- cashouts
- </a>
- <br />
- <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
- e.preventDefault();
- onAction("remove-account", item.username)
- }}
- >
- remove
- </a>
+ {config.have_cashout ?
+ <Fragment>
+
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onShowCashoutForAccount(item.username)
+ }}
+ >
+ cashouts
+ </a>
+ <br />
+ </Fragment>
+ : undefined}
+ {noBalance ?
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onRemoveAccount(item.username)
+ }}
+ >
+ remove
+ </a>
+ : undefined}
</td>
</tr>
})}
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
new file mode 100644
index 000000000..01f9f6dbd
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -0,0 +1,32 @@
+import { Fragment, VNode, h } from "preact";
+import { Transactions } from "../../components/Transactions/index.js";
+import { WireTransfer } from "./Account.js";
+import { AccountList } from "./AccountList.js";
+
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+interface Props {
+ onRegister: () => void;
+
+ onCreateAccount: () => void;
+ onShowAccountDetails: (aid: string) => void;
+ onRemoveAccount: (aid: string) => void;
+ onUpdateAccountPassword: (aid: string) => void;
+ onShowCashoutForAccount: (aid: string) => void;
+}
+export function AdminHome({ onCreateAccount, onRegister, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode {
+ return <Fragment>
+ <AccountList
+ onCreateAccount={onCreateAccount}
+ onRemoveAccount={onRemoveAccount}
+ onShowCashoutForAccount={onShowCashoutForAccount}
+ onShowAccountDetails={onShowAccountDetails}
+ onUpdateAccountPassword={onUpdateAccountPassword}
+ />
+
+ <WireTransfer onRegister={onRegister} />
+
+ <Transactions account="admin" />
+ </Fragment>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx
new file mode 100644
index 000000000..466dc1a4b
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/CashoutListForAccount.tsx
@@ -0,0 +1,47 @@
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+
+interface Props {
+ account: string,
+ onClose: () => void,
+ onSelected: (cid: string) => void
+}
+
+export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const { state: credentials } = useBackendState();
+ const token = credentials.status !== "loggedIn" ? undefined : credentials.token
+
+ const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
+ credentials.username === account : false
+
+ return <Fragment>
+ {accountIsTheCurrentUser ?
+ <ProfileNavigation current="cashouts" />
+ :
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cashout for account {account}</i18n.Translate>
+ </h1>
+ }
+ <Cashouts
+ account={account}
+ onSelected={onSelected}
+ />
+ <p>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose();
+ }}
+ />
+ </p>
+ </Fragment>
+}
+
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index e10c3ad41..772ea6e84 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -1,21 +1,22 @@
import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../../utils.js";
import { getRandomPassword } from "../rnd.js";
-import { AccountForm } from "./AccountForm.js";
+import { AccountForm, AccountFormData } from "./AccountForm.js";
import { useBackendState } from "../../hooks/backend.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { assertUnreachable } from "../HomePage.js";
import { mutate } from "swr";
+import { Attention } from "../../components/Attention.js";
export function CreateNewAccount({
onCancel,
onCreateSuccess,
}: {
onCancel: () => void;
- onCreateSuccess: (password: string) => void;
+ onCreateSuccess: () => void;
}): VNode {
const { i18n } = useTranslationContext();
// const { createAccount } = useAdminAccountAPI();
@@ -23,9 +24,7 @@ export function CreateNewAccount({
const token = credentials.status !== "loggedIn" ? undefined : credentials.token
const { api } = useBankCoreApiContext();
- const [submitAccount, setSubmitAccount] = useState<
- TalerCorebankApi.AccountData | undefined
- >();
+ const [submitAccount, setSubmitAccount] = useState<AccountFormData | undefined>();
async function doCreate() {
if (!submitAccount || !token) return;
@@ -35,14 +34,17 @@ export function CreateNewAccount({
challenge_contact_data: submitAccount.contact_data,
internal_payto_uri: submitAccount.payto_uri,
name: submitAccount.name,
- username: "",//FIXME: not in account data
+ username: submitAccount.username,//FIXME: not in account data
password: getRandomPassword(),
};
const resp = await api.createAccount(token, account);
if (resp.type === "ok") {
mutate(() => true)// clean account list
- onCreateSuccess(account.password);
+ notifyInfo(
+ i18n.str`Account created with password "${account.password}". The user must change the password on the next login.`,
+ );
+ onCreateSuccess();
} else {
switch (resp.case) {
case "invalid-input": return notify({
@@ -75,6 +77,12 @@ export function CreateNewAccount({
})
}
+ if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) {
+ return <Attention type="warning" title={i18n.str`Can't create accounts`} onClose={onCancel}>
+ <i18n.Translate>Only system admin can create accounts.</i18n.Translate>
+ </Attention>
+ }
+
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx
deleted file mode 100644
index 71ea8ce1b..000000000
--- a/packages/demobank-ui/src/pages/admin/Home.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { Cashouts } from "../../components/Cashouts/index.js";
-import { Transactions } from "../../components/Transactions/index.js";
-import { ShowAccountDetails } from "../ShowAccountDetails.js";
-import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
-import { ShowCashoutDetails } from "../business/Home.js";
-import { AdminAccount } from "./Account.js";
-import { AccountList } from "./AccountList.js";
-import { CreateNewAccount } from "./CreateNewAccount.js";
-import { RemoveAccount } from "./RemoveAccount.js";
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-interface Props {
- onRegister: () => void;
-}
-export type AccountAction = "show-details" |
- "show-cashout" |
- "update-password" |
- "remove-account" |
- "show-cashouts-details";
-
-export function AdminHome({ onRegister }: Props): VNode {
- const [action, setAction] = useState<{
- type: AccountAction,
- account: string
- } | undefined>()
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const { i18n } = useTranslationContext();
-
- if (action) {
- switch (action.type) {
- case "show-cashouts-details": return <ShowCashoutDetails
- id={action.account}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "show-cashout": return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Cashout for account {action.account}</i18n.Translate>
- </h1>
- </div>
- <Cashouts
- account={action.account}
- onSelected={(id) => {
- setAction({
- type: "show-cashouts-details",
- account: action.account
- });
- }}
- />
- <p>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- setAction(undefined);
- }}
- />
- </p>
- </div>
- )
- case "update-password": return <UpdateAccountPassword
- account={action.account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setAction(undefined);
- }}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "remove-account": return <RemoveAccount
- account={action.account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account removed`);
- setAction(undefined);
- }}
- onCancel={() => {
- setAction(undefined);
- }}
- />
- case "show-details": return <ShowAccountDetails
- account={action.account}
- onChangePassword={() => {
- setAction({
- type: "update-password",
- account: action.account,
- })
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setAction(undefined);
- }}
- onClear={() => {
- setAction(undefined);
- }}
- />
- }
- }
-
- if (createAccount) {
- return (
- <CreateNewAccount
- onCancel={() => setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- setCreateAccount(false);
- }}
- />
- );
- }
-
- return (
- <Fragment>
-
- <AccountList
- onCreateAccount={() => {
- setCreateAccount(true);
- }}
- account={undefined}
- onAction={(type, account) => setAction({ account, type })}
-
- />
-
- <AdminAccount onRegister={onRegister} />
-
- <Transactions account="admin" />
- </Fragment>
- );
-} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 9a212ebd0..88961c2cb 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,5 +1,5 @@
import { Amounts, HttpStatusCode, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpResponsePaginated, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Attention } from "../../components/Attention.js";
@@ -63,6 +63,7 @@ export function RemoveAccount({
await withRuntimeErrorHandling(i18n, async () => {
const resp = await api.deleteAccount({ username: account, token });
if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account removed`);
onUpdateSuccess();
} else {
switch (resp.case) {
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index d7beda01d..4696c899e 100644
--- a/packages/demobank-ui/src/pages/business/Home.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -21,15 +21,12 @@ import {
} from "@gnu-taler/taler-util";
import {
notify,
- notifyError,
- notifyInfo,
useTranslationContext
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
-import { Cashouts } from "../../components/Cashouts/index.js";
import { ErrorLoading } from "../../components/ErrorLoading.js";
import { Loading } from "../../components/Loading.js";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
@@ -43,114 +40,15 @@ import {
} from "../../hooks/circuit.js";
import {
TanChannel,
- buildRequestErrorMessage,
undefinedIfEmpty,
- withRuntimeErrorHandling,
+ withRuntimeErrorHandling
} from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
import { InputAmount } from "../PaytoWireTransferForm.js";
-import { ShowAccountDetails } from "../ShowAccountDetails.js";
-import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
+import { assertUnreachable } from "../HomePage.js";
+import { Attention } from "../../components/Attention.js";
interface Props {
- account: string,
- onClose: () => void;
- onRegister: () => void;
-}
-export function BusinessAccount({
- onClose,
- account,
- onRegister,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const [updatePassword, setUpdatePassword] = useState(false);
- const [newCashout, setNewcashout] = useState(false);
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- if (newCashout) {
- return (
- <CreateCashout
- account={account}
- onCancel={() => {
- setNewcashout(false);
- }}
- onComplete={(id) => {
- notifyInfo(
- i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`,
- );
- setNewcashout(false);
- setShowCashoutDetails(id);
- }}
- />
- );
- }
- if (showCashoutDetails) {
- return (
- <ShowCashoutDetails
- id={showCashoutDetails}
- onCancel={() => {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- <UpdateAccountPassword
- account={account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(false);
- }}
- onCancel={() => {
- setUpdatePassword(false);
- }}
- />
- );
- }
- return (
- <div>
- <ShowAccountDetails
- account={account}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- }}
- onChangePassword={() => {
- setUpdatePassword(true);
- }}
- onClear={onClose}
- />
- <section style={{ marginTop: "2em" }}>
- <div class="active">
- <h3>{i18n.str`Latest cashouts`}</h3>
- <Cashouts
- account={account}
- onSelected={(id) => {
- setShowCashoutDetails(id);
- }}
- />
- </div>
- <br />
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div />
- <input
- class="pure-button pure-button-primary content"
- type="submit"
- value={i18n.str`New cashout`}
- onClick={async (e) => {
- e.preventDefault();
- setNewcashout(true);
- }}
- />
- </div>
- </section>
- </div>
- );
-}
-
-interface PropsCashout {
account: string;
onComplete: (id: string) => void;
onCancel: () => void;
@@ -167,11 +65,11 @@ type ErrorFrom<T> = {
};
-function CreateCashout({
+export function CreateCashout({
account: accountName,
onComplete,
onCancel,
-}: PropsCashout): VNode {
+}: Props): VNode {
const { i18n } = useTranslationContext();
const resultRatios = useRatiosAndFeeConfig();
const resultAccount = useAccountDetails(accountName);
@@ -184,6 +82,17 @@ function CreateCashout({
const { api, config } = useBankCoreApiContext()
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+ if (!config.have_cashout) {
+ return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}>
+ <i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate>
+ </Attention>
+ }
+ if (!config.fiat_currency) {
+ return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}>
+ <i18n.Translate>The bank configuration support cashout operations but there is no fiat currency.</i18n.Translate>
+ </Attention>
+ }
+
if (!resultAccount || !resultRatios) {
return <Loading />
}
@@ -207,9 +116,6 @@ function CreateCashout({
default: assertUnreachable(resultRatios.case)
}
}
- if (!config.fiat_currency) {
- return <div>cashout operations are not supported</div>
- }
const ratio = resultRatios.body
@@ -514,223 +420,3 @@ function CreateCashout({
</div>
);
}
-
-interface ShowCashoutProps {
- id: string;
- onCancel: () => void;
-}
-export function ShowCashoutDetails({
- id,
- onCancel,
-}: ShowCashoutProps): VNode {
- const { i18n } = useTranslationContext();
- const { state } = useBackendState();
- const creds = state.status !== "loggedIn" ? undefined : state
- const { api } = useBankCoreApiContext()
- const result = useCashoutDetails(id);
- const [code, setCode] = useState<string | undefined>(undefined);
-
- if (!result) {
- return <Loading />
- }
- if (result instanceof TalerError) {
- return <ErrorLoading error={result} />
- }
- if (result.type === "fail") {
- switch (result.case) {
- case "already-aborted": return <div>this cashout is already aborted</div>
- default: assertUnreachable(result.case)
- }
- }
- const errors = undefinedIfEmpty({
- code: !code ? i18n.str`required` : undefined,
- });
- const isPending = String(result.body.status).toUpperCase() === "PENDING";
- return (
- <div>
- <h1>Cashout details {id}</h1>
- <form class="pure-form">
- <fieldset>
- <label>
- <i18n.Translate>Subject</i18n.Translate>
- </label>
- <input readOnly value={result.body.subject} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Created</i18n.Translate>
- </label>
- <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Confirmed</i18n.Translate>
- </label>
- <input readOnly value={result.body.confirmation_time === undefined ? "-" :
- (result.body.confirmation_time.t_s === "never" ?
- i18n.str`never` :
- format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
- } />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Debited</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_debit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Credit</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_credit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Status</i18n.Translate>
- </label>
- <input readOnly value={result.body.status} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Destination</i18n.Translate>
- </label>
- <input readOnly value={result.body.credit_payto_uri} />
- </fieldset>
- {isPending ? (
- <fieldset>
- <label>
- <i18n.Translate>Code</i18n.Translate>
- </label>
- <input
- value={code ?? ""}
- onChange={(e) => {
- setCode(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.code}
- isDirty={code !== undefined}
- />
- </fieldset>
- ) : undefined}
- </form>
- <br />
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onCancel();
- }}
- >
- {i18n.str`Back`}
- </button>
- {isPending ? (
- <div>
- <button
- type="submit"
- class="pure-button pure-button-primary button-error"
- onClick={async (e) => {
- e.preventDefault();
- if (!creds) return;
- await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.abortCashoutById(creds, id);
- if (resp.type === "ok") {
- onCancel();
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "already-confirmed": return notify({
- type: "error",
- title: i18n.str`Cashout was already confimed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: {
- assertUnreachable(resp)
- }
- }
- }
- })
- }}
- >
- {i18n.str`Abort`}
- </button>
- &nbsp;
- <button
- type="submit"
- disabled={!code}
- class="pure-button pure-button-primary "
- onClick={async (e) => {
- e.preventDefault();
- if (!creds || !code) return;
- await withRuntimeErrorHandling(i18n, async () => {
- const resp = await api.confirmCashoutById(creds, id, {
- tan: code,
- });
- if (resp.type === "ok") {
- mutate(() => true)//clean cashout state
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "wrong-tan-or-credential": return notify({
- type: "error",
- title: i18n.str`Invalid code or credentials.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "cashout-address-changed": return notify({
- type: "error",
- title: i18n.str`The cash-out address between the creation and the confirmation changed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
- }
- }
- })
- }}
- >
- {i18n.str`Confirm`}
- </button>
- </div>
- ) : (
- <div />
- )}
- </div>
- </div>
- );
-}
-
-const MAX_AMOUNT_DIGIT = 2;
-/**
- * Truncate the amount of digits to display
- * in the form based on the fee calculations
- *
- * Backend must have the same truncation
- * @param a
- * @returns
- */
-function truncate(a: AmountJson): AmountJson {
- const str = Amounts.stringify(a);
- const idx = str.indexOf(".");
- if (idx === -1) {
- return a;
- }
- const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
- return Amounts.parseOrThrow(truncated);
-}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
new file mode 100644
index 000000000..a8e34e4b9
--- /dev/null
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -0,0 +1,237 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ TalerError,
+ TranslatedString
+} from "@gnu-taler/taler-util";
+import {
+ notify,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { mutate } from "swr";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Loading } from "../../components/Loading.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useBankCoreApiContext } from "../../context/config.js";
+import { useBackendState } from "../../hooks/backend.js";
+import {
+ useCashoutDetails
+} from "../../hooks/circuit.js";
+import {
+ undefinedIfEmpty,
+ withRuntimeErrorHandling
+} from "../../utils.js";
+import { assertUnreachable } from "../HomePage.js";
+
+interface Props {
+ id: string;
+ onCancel: () => void;
+}
+export function ShowCashoutDetails({
+ id,
+ onCancel,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { state } = useBackendState();
+ const creds = state.status !== "loggedIn" ? undefined : state
+ const { api } = useBankCoreApiContext()
+ const result = useCashoutDetails(id);
+ const [code, setCode] = useState<string | undefined>(undefined);
+
+ if (!result) {
+ return <Loading />
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoading error={result} />
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case "already-aborted": return <div>this cashout is already aborted</div>
+ default: assertUnreachable(result.case)
+ }
+ }
+ const errors = undefinedIfEmpty({
+ code: !code ? i18n.str`required` : undefined,
+ });
+ const isPending = String(result.body.status).toUpperCase() === "PENDING";
+ return (
+ <div>
+ <h1>Cashout details {id}</h1>
+ <form class="pure-form">
+ <fieldset>
+ <label>
+ <i18n.Translate>Subject</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.subject} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Created</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Confirmed</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.confirmation_time === undefined ? "-" :
+ (result.body.confirmation_time.t_s === "never" ?
+ i18n.str`never` :
+ format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
+ } />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Debited</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.amount_debit} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Credit</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.amount_credit} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Status</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.status} />
+ </fieldset>
+ <fieldset>
+ <label>
+ <i18n.Translate>Destination</i18n.Translate>
+ </label>
+ <input readOnly value={result.body.credit_payto_uri} />
+ </fieldset>
+ {isPending ? (
+ <fieldset>
+ <label>
+ <i18n.Translate>Code</i18n.Translate>
+ </label>
+ <input
+ value={code ?? ""}
+ onChange={(e) => {
+ setCode(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.code}
+ isDirty={code !== undefined}
+ />
+ </fieldset>
+ ) : undefined}
+ </form>
+ <br />
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={(e) => {
+ e.preventDefault();
+ onCancel();
+ }}
+ >
+ {i18n.str`Back`}
+ </button>
+ {isPending ? (
+ <div>
+ <button
+ type="submit"
+ class="pure-button pure-button-primary button-error"
+ onClick={async (e) => {
+ e.preventDefault();
+ if (!creds) return;
+ await withRuntimeErrorHandling(i18n, async () => {
+ const resp = await api.abortCashoutById(creds, id);
+ if (resp.type === "ok") {
+ onCancel();
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "already-confirmed": return notify({
+ type: "error",
+ title: i18n.str`Cashout was already confimed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: {
+ assertUnreachable(resp)
+ }
+ }
+ }
+ })
+ }}
+ >
+ {i18n.str`Abort`}
+ </button>
+ &nbsp;
+ <button
+ type="submit"
+ disabled={!code}
+ class="pure-button pure-button-primary "
+ onClick={async (e) => {
+ e.preventDefault();
+ if (!creds || !code) return;
+ await withRuntimeErrorHandling(i18n, async () => {
+ const resp = await api.confirmCashoutById(creds, id, {
+ tan: code,
+ });
+ if (resp.type === "ok") {
+ mutate(() => true)//clean cashout state
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "wrong-tan-or-credential": return notify({
+ type: "error",
+ title: i18n.str`Invalid code or credentials.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "cashout-address-changed": return notify({
+ type: "error",
+ title: i18n.str`The cash-out address between the creation and the confirmation changed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: assertUnreachable(resp)
+ }
+ }
+ })
+ }}
+ >
+ {i18n.str`Confirm`}
+ </button>
+ </div>
+ ) : (
+ <div />
+ )}
+ </div>
+ </div>
+ );
+}