This commit is contained in:
Sebastian 2023-09-29 16:02:15 -03:00
parent c10f3f3ade
commit 1708d49a2d
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
24 changed files with 448 additions and 470 deletions

View File

@ -18,7 +18,7 @@
import { serve } from "@gnu-taler/web-util/node"; import { serve } from "@gnu-taler/web-util/node";
import { initializeDev } from "@gnu-taler/web-util/build"; import { initializeDev } from "@gnu-taler/web-util/build";
const devEntryPoints = ["src/stories.tsx", "src/index.tsx"]; const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/demobank-ui-settings.js"];
const build = initializeDev({ const build = initializeDev({
type: "development", type: "development",

View File

@ -0,0 +1,59 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { assertUnreachable } from "./Routing.js";
interface Props {
type?: "info" | "success" | "warning" | "danger",
onClose?: () => void,
title: TranslatedString,
children?: ComponentChildren ,
}
export function Attention({ type = "info", title, children, onClose }: Props): VNode {
return <div class={`group attention-${type} mt-2`}>
<div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow">
<div class="flex">
<div >
<svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400">
{(() => {
switch (type) {
case "info":
return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" />
case "warning":
return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
case "danger":
return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
case "success":
return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" />
default:
assertUnreachable(type)
}
})()}
</svg>
</div>
<div class="ml-3 w-full">
<h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800">
{title}
</h3>
<div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700">
{children}
</div>
</div>
{onClose &&
<div>
<button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50"
onClick={(e) => {
e.preventDefault();
onClose();
}}
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</div>
}
</div>
</div>
</div>
}

View File

@ -17,25 +17,13 @@
import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { Attention } from "./Attention.js";
import { TranslatedString } from "@gnu-taler/taler-util";
export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode { export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode {
const { i18n } = useTranslationContext() const { i18n } = useTranslationContext()
return ( return (<Attention type="danger" title={error.message as TranslatedString}>
<div><div class="rounded-md bg-red-50 p-4"> <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
<div class="flex"> </Attention>
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" 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 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm font-medium text-red-800">{error.message}</p>
</div>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
</div>
</div>
</div>
); );
} }

View File

@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js"; import { State } from "./index.js";
import { format, isToday } from "date-fns"; import { format, isToday } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { useEffect, useRef } from "preact/hooks";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -55,9 +56,9 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<thead> <thead>
<tr> <tr>
<th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th> <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th>
<th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Amount`}</th> <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th>
<th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Counterpart`}</th> <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th>
<th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Subject`}</th> <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -69,22 +70,38 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
</th> </th>
</tr> </tr>
{txs.map(item => { {txs.map(item => {
const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss")
const amount = <Fragment>
{item.negative ? "-" : ""}
{item.amount ? (
`${Amounts.stringifyValue(item.amount)} ${item.amount.currency
}`
) : (
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}
</Fragment>
return (<tr key={idx}> return (<tr key={idx}>
<td class="relative py-2 pl-2 pr-2 text-sm "> <td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">{item.when.t_ms === "never" <div class="font-medium text-gray-900">{time}</div>
? "" <dl class="font-normal sm:hidden">
: format(item.when.t_ms, "HH:mm:ss")}</div> <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
<dd class="mt-1 truncate text-gray-700">
{item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
`${Amounts.stringifyValue(item.amount)}`
) : (
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}</dd>
<dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
<dd class="mt-1 truncate text-gray-500 sm:hidden">
{item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
</dd>
</dl>
</td> </td>
<td data-negative={item.negative ? "true" : "false"} <td data-negative={item.negative ? "true" : "false"}
class="px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> 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">
{item.negative ? "-" : ""} {amount}
{item.amount ? ( </td>
`${Amounts.stringifyValue(item.amount)} ${item.amount.currency <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
}`
) : (
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}</td>
<td class="px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
<td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td> <td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td>
</tr>) </tr>)
})} })}
@ -94,8 +111,8 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
</tbody> </tbody>
</table> </table>
<nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination"> <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
<div class="flex flex-1 justify-between sm:justify-end"> <div class="flex flex-1 justify-between sm:justify-end">
<button <button
class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"

View File

@ -0,0 +1,21 @@
// Values for development environment
/**
* Global settings for the demobank UI.
*/
localStorage.setItem("bank-base-url", "http://bank.taler.test/");
globalThis.talerDemobankSettings = {
backendBaseURL: "http://bank.taler.test/",
allowRegistrations: true,
showDemoNav: true,
simplePasswordForRandomAccounts: true,
allowRandomAccountCreation: true,
bankName: "Taler DEVELOPMENT Bank",
// Names and links for other demo sites to show in the navbar
demoSites: [
["Exchange", "https://Exchnage.taler.test/"],
["Bank", "https://bank-ui.taler.test/"],
["Merchant", "https://merchant.taler.test/"],
],
};

View File

@ -70,7 +70,7 @@ export function useAccessAPI(): AccessAPI {
contentType: "json", contentType: "json",
}, },
); );
await mutateAll(/.*accounts\/.*\/transactions.*/); await mutateAll(/.*accounts\/.*/);
return res; return res;
}; };
const deleteAccount = async (): Promise<HttpResponseOk<void>> => { const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
@ -382,7 +382,6 @@ export function useTransactions(
loadMore: () => { loadMore: () => {
if (!afterData || isReachingEnd) return; if (!afterData || isReachingEnd) return;
// if (afterData.data.transactions.length < MAX_RESULT_SIZE) { // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
// console.log("load more", page)
const l = afterData.data.transactions[afterData.data.transactions.length-1] const l = afterData.data.transactions[afterData.data.transactions.length-1]
setStart(String(l.row_id)); setStart(String(l.row_id));
// } // }

View File

@ -435,7 +435,7 @@ export function useBusinessAccounts(
HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>,
RequestError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>( >(
[`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], [`accounts`, args?.page, PAGE_SIZE, args?.account],
sandboxAccountsFetcher, sandboxAccountsFetcher,
{ {
refreshInterval: 0, refreshInterval: 0,

View File

@ -33,6 +33,7 @@ interface Settings {
showInstallWallet: boolean; showInstallWallet: boolean;
maxWithdrawalAmount: number; maxWithdrawalAmount: number;
fastWithdrawal: boolean; fastWithdrawal: boolean;
showDebugInfo: boolean;
} }
export const codecForSettings = (): Codec<Settings> => export const codecForSettings = (): Codec<Settings> =>
@ -42,6 +43,7 @@ export const codecForSettings = (): Codec<Settings> =>
.property("showDemoDescription", (codecForBoolean())) .property("showDemoDescription", (codecForBoolean()))
.property("showInstallWallet", (codecForBoolean())) .property("showInstallWallet", (codecForBoolean()))
.property("fastWithdrawal", (codecForBoolean())) .property("fastWithdrawal", (codecForBoolean()))
.property("showDebugInfo", (codecForBoolean()))
.property("maxWithdrawalAmount", codecForNumber()) .property("maxWithdrawalAmount", codecForNumber())
.build("Settings"); .build("Settings");
@ -52,6 +54,7 @@ const defaultSettings: Settings = {
showInstallWallet: true, showInstallWallet: true,
maxWithdrawalAmount: 25, maxWithdrawalAmount: 25,
fastWithdrawal: false, fastWithdrawal: false,
showDebugInfo: false,
}; };
const DEMOBANK_SETTINGS_KEY = buildStorageKey( const DEMOBANK_SETTINGS_KEY = buildStorageKey(

View File

@ -16,10 +16,10 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact"; import { Fragment, VNode, h } from "preact";
import { Attention } from "../../components/Attention.js";
import { Transactions } from "../../components/Transactions/index.js"; import { Transactions } from "../../components/Transactions/index.js";
import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js";
import { useSettings } from "../../hooks/settings.js"; import { useSettings } from "../../hooks/settings.js";
import { bankUiSettings } from "../../settings.js";
import { PaymentOptions } from "../PaymentOptions.js"; import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js"; import { State } from "./index.js";
@ -31,53 +31,27 @@ export function InvalidIbanView({ error }: State.InvalidIban) {
const IS_PUBLIC_ACCOUNT_ENABLED = false const IS_PUBLIC_ACCOUNT_ENABLED = false
function ShowDemoInfo(): VNode { function ShowDemoInfo(): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
if (!settings.showDemoDescription) return <Fragment /> if (!settings.showDemoDescription) return <Fragment />
return <div class="rounded-md bg-blue-50 p-4"> return <Attention title={i18n.str`This is a demo bank`} onClose={() => {
<div class="flex"> updateSettings("showDemoDescription", false);
<div class="flex-shrink-0"> }}>
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> {IS_PUBLIC_ACCOUNT_ENABLED ? (
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> <i18n.Translate>
</svg> This part of the demo shows how a bank that supports Taler
</div> directly would work. In addition to using your own bank
<div class="ml-3"> account, you can also see the transaction history of some{" "}
<h3 class="text-sm font-bold text-blue-800"> <a href="/public-accounts">Public Accounts</a>.
<i18n.Translate>This is a demo bank!</i18n.Translate> </i18n.Translate>
</h3> ) : (
<div class="mt-2 text-sm text-blue-700"> <i18n.Translate>
{IS_PUBLIC_ACCOUNT_ENABLED ? ( This part of the demo shows how a bank that supports Taler
<i18n.Translate> directly would work.
This part of the demo shows how a bank that supports Taler </i18n.Translate>
directly would work. In addition to using your own bank )}
account, you can also see the transaction history of some{" "} </Attention>
<a href="/public-accounts">Public Accounts</a>.
</i18n.Translate>
) : (
<i18n.Translate>
This part of the demo shows how a bank that supports Taler
directly would work.
</i18n.Translate>
)}
<p class="mt-3 text-sm flex justify-end">
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={(e) => {
e.preventDefault();
updateSettings("showDemoDescription", false);
}}
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</p>
</div>
</div>
</div>
</div>
} }
export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {

View File

@ -15,7 +15,7 @@
*/ */
import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; 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 { ComponentChildren, Fragment, h, VNode } from "preact";
import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks";
import { LangSelector } from "../components/LangSelector.js"; import { LangSelector } from "../components/LangSelector.js";
@ -26,6 +26,7 @@ import { useSettings } from "../hooks/settings.js";
import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import { CopyButton, CopyIcon } from "../components/CopyButton.js";
import logo from "../assets/logo-2021.svg"; import logo from "../assets/logo-2021.svg";
import { useAccountDetails } from "../hooks/access.js"; import { useAccountDetails } from "../hooks/access.js";
import { Attention } from "../components/Attention.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@ -108,7 +109,7 @@ export function BankFrame({
setOpen(!open) setOpen(!open)
}}> }}>
<span class="absolute -inset-0.5"></span> <span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span> <span class="sr-only">Open settings</span>
<svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg> </svg>
@ -227,6 +228,22 @@ export function BankFrame({
</button> </button>
</div> </div>
</li> </li>
<li class="mt-2">
<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>Show debug info</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={settings.showDebugInfo} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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"
onClick={() => {
updateSettings("showDebugInfo", !settings.showDebugInfo);
}}>
<span aria-hidden="true" data-enabled={settings.showDebugInfo} class="translate-x-5 data-[enabled=false]: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>
</li>
<li class="mt-2"> <li class="mt-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex flex-grow flex-col"> <span class="flex flex-grow flex-col">
@ -286,10 +303,10 @@ export function BankFrame({
} }
</div > </div >
<StatusBanner />
<main class="-mt-32 flex-1"> <main class="-mt-32 flex-1">
<div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
<div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6"> <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
<StatusBanner />
{children} {children}
</div> </div>
</div> </div>
@ -301,79 +318,46 @@ export function BankFrame({
); );
} }
function MaybeShowDebugInfo({ info }: { info: any }): VNode {
const [settings] = useSettings()
if (settings.showDebugInfo) {
return <pre class="whitespace-break-spaces ">
{info}
</pre>
}
return <Fragment />
}
function StatusBanner(): VNode { function StatusBanner(): VNode {
const notifs = useNotifications() const notifs = useNotifications()
return <div if (notifs.length === 0) return <Fragment />
class="fixed top-10 z-20 ml-4 mr-4" return <div class="fixed z-20 w-full p-4"> {
> {
notifs.map(n => { notifs.map(n => {
switch (n.message.type) { switch (n.message.type) {
case "error": case "error":
return <div class="rounded-md bg-red-50 p-4"> return <Attention type="danger" title={n.message.title} onClose={() => {
<div class="flex"> n.remove()
<div class="flex-shrink-0"> }}>
<svg class="h-5 w-5 text-red-400" 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 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm font-medium text-red-800">{n.message.title}</p>
</div>
<div>
<p class="text-sm">
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={(e) => {
e.preventDefault();
n.remove()
}}
>
Close
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</p>
</div>
</div>
{n.message.description && {n.message.description &&
<div class="mt-2 text-sm text-red-700"> <div class="mt-2 text-sm text-red-700">
{n.message.description} {n.message.description}
</div> </div>
} }
<MaybeShowDebugInfo info={n.message.debug} />
{/* <a href="#" class="text-gray-500">
show debug info
</a>
{n.message.debug && {n.message.debug &&
<div class="mt-2 text-sm text-red-700 font-mono break-all"> <div class="mt-2 text-sm text-red-700 font-mono break-all">
{n.message.debug} {n.message.debug}
</div> </div>
} } */}
</div> </Attention>
case "info": case "info":
return <div class="rounded-md bg-green-50 border-4 border-green-600 p-6"> return <Attention type="success" title={n.message.title} onClose={() => {
<div class="flex"> n.remove();
<div class="flex-shrink-0"> }} />
<svg class="h-8 w-8 text-green-400" 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" />
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<h3 class="text-lg font-medium text-green-800">{n.message.title}</h3>
<p class="mt-3 text-sm md:ml-6 md:mt-0">
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-md text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={(e) => {
e.preventDefault();
n.remove();
}}
>
<svg class="h-8 w-8" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</p>
</div>
</div>
</div>
} }
})} })}
</div> </div>

View File

@ -137,8 +137,8 @@ export function handleNotOkResult(
const errorData = result.payload; const errorData = result.payload;
notify({ notify({
type: "error", type: "error",
title: i18n.str`Could not load due to a client error`, title: i18n.str`Could not load due to a request error`,
description: errorData?.error?.description as TranslatedString, description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`,
debug: JSON.stringify(result), debug: JSON.stringify(result),
}); });
break; break;
@ -174,7 +174,7 @@ export function handleNotOkResult(
assertUnreachable(result); assertUnreachable(result);
} }
} }
route("/") // route("/")
return <div>error</div>; return <div>error</div>;
} }
return <div />; return <div />;

View File

@ -23,6 +23,7 @@ import { useBackendContext } from "../context/backend.js";
import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
import { bankUiSettings } from "../settings.js"; import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js"; import { undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
/** /**
@ -98,8 +99,8 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
}); });
} else { } else {
saveError({ saveError({
title: i18n.str`Could not load due to a client error`, title: i18n.str`Could not load due to a request error`,
// description: cause.payload.error.description, description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`,
debug: JSON.stringify(cause.payload), debug: JSON.stringify(cause.payload),
}); });
} }
@ -159,8 +160,7 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input
ref={ref} ref={doAutoFocus}
autoFocus
type="text" type="text"
name="username" name="username"
id="username" id="username"

View File

@ -118,7 +118,9 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive
try { try {
setBusy({}) setBusy({})
await confirmWithdrawal(wid); await confirmWithdrawal(wid);
notifyInfo(i18n.str`Wire transfer completed!`) if (!settings.showWithdrawalSuccess) {
notifyInfo(i18n.str`Wire transfer completed!`)
}
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
notify( notify(

View File

@ -267,13 +267,12 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) {
</div> </div>
<div class="mt-3 text-center sm:mt-5"> <div class="mt-3 text-center sm:mt-5">
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
<i18n.Translate>Withdrawal OK</i18n.Translate> <i18n.Translate>Withdrawal confirmed</i18n.Translate>
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<i18n.Translate> <i18n.Translate>
The wire transfer to the Taler exchange bank's account is completed, now the The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate> </i18n.Translate>
</p> </p>
</div> </div>

View File

@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [settings] = useSettings(); const [settings] = useSettings();
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer");
return ( return (
<div class="mt-2"> <div class="mt-2">
@ -82,7 +82,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
<i18n.Translate>another bank account</i18n.Translate> <i18n.Translate>another bank account</i18n.Translate>
</span> </span>
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
<i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate> <i18n.Translate>Make a wire transfer to an account which you know the bank account number</i18n.Translate>
</span> </span>
</span> </span>
</span> </span>
@ -108,6 +108,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
limit={limit} limit={limit}
onSuccess={() => { onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`); notifyInfo(i18n.str`Wire transfer created!`);
setTab(undefined)
}} }}
onCancel={() => { onCancel={() => {
setTab(undefined) setTab(undefined)

View File

@ -55,10 +55,11 @@ export function PaytoWireTransferForm({
onCancel: (() => void) | undefined; onCancel: (() => void) | undefined;
limit: AmountJson; limit: AmountJson;
}): VNode { }): VNode {
const [isRawPayto, setIsRawPayto] = useState(false); const [isRawPayto, setIsRawPayto] = useState(true);
const [iban, setIban] = useState<string | undefined>(undefined); // FIXME: remove this
const [subject, setSubject] = useState<string | undefined>(undefined); const [iban, setIban] = useState<string | undefined>("DE4745461198061");
const [amount, setAmount] = useState<string | undefined>(undefined); const [subject, setSubject] = useState<string | undefined>("ASD");
const [amount, setAmount] = useState<string | undefined>("1.00001");
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>( const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined, undefined,
@ -76,17 +77,17 @@ export function PaytoWireTransferForm({
const errorsWire = undefinedIfEmpty({ const errorsWire = undefinedIfEmpty({
iban: !iban iban: !iban
? i18n.str`Missing IBAN` ? i18n.str`required`
: !IBAN_REGEX.test(iban) : !IBAN_REGEX.test(iban)
? i18n.str`IBAN should have just uppercased letters and numbers` ? i18n.str`IBAN should have just uppercased letters and numbers`
: validateIBAN(iban, i18n), : validateIBAN(iban, i18n),
subject: !subject ? i18n.str`Missing subject` : undefined, subject: !subject ? i18n.str`required` : undefined,
amount: !trimmedAmountStr amount: !trimmedAmountStr
? i18n.str`Missing amount` ? i18n.str`required`
: !parsedAmount : !parsedAmount
? i18n.str`Amount is not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedAmount) : Amounts.isZero(parsedAmount)
? i18n.str`Should be greater than 0` ? i18n.str`should be greater than 0`
: Amounts.cmp(limit, parsedAmount) === -1 : Amounts.cmp(limit, parsedAmount) === -1
? i18n.str`balance is not enough` ? i18n.str`balance is not enough`
: undefined, : undefined,
@ -101,14 +102,14 @@ export function PaytoWireTransferForm({
? i18n.str`required` ? i18n.str`required`
: !parsed : !parsed
? i18n.str`does not follow the pattern` ? i18n.str`does not follow the pattern`
: !parsed.params.amount : !parsed.isKnown || parsed.targetType !== "iban"
? i18n.str`use the "amount" parameter to specify the amount to be transferred` ? i18n.str`only "IBAN" target are supported`
: Amounts.parse(parsed.params.amount) === undefined : !parsed.params.amount
? i18n.str`the amount is not valid` ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
: !parsed.params.message : Amounts.parse(parsed.params.amount) === undefined
? i18n.str`use the "message" parameter to specify a reference text for the transfer` ? i18n.str`the amount is not valid`
: !parsed.isKnown || parsed.targetType !== "iban" : !parsed.params.message
? i18n.str`only "IBAN" target are supported` ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
: !IBAN_REGEX.test(parsed.iban) : !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers` ? i18n.str`IBAN should have just uppercased letters and numbers`
: validateIBAN(parsed.iban, i18n), : validateIBAN(parsed.iban, i18n),
@ -159,6 +160,9 @@ export function PaytoWireTransferForm({
} }
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"> 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">
{/**
* FIXME: Scan a qr code
*/}
<div class="px-4 sm:px-0"> <div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900"> <h2 class="text-base font-semibold leading-7 text-gray-900">
{title} {title}
@ -167,6 +171,17 @@ export function PaytoWireTransferForm({
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4"> <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4">
<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")}> <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="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { <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 subject = parsed.params["subject"]
if (subject) {
setSubject(subject)
}
}
setIsRawPayto(false) setIsRawPayto(false)
}} /> }} />
<span class="flex flex-1"> <span class="flex flex-1">
@ -180,12 +195,22 @@ export function PaytoWireTransferForm({
<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")}> <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={() => { <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))
}
setIsRawPayto(true) setIsRawPayto(true)
}} /> }} />
<span class="flex flex-1"> <span class="flex flex-1">
<span class="flex flex-col"> <span class="flex flex-col">
<span class="block text-sm font-medium text-gray-900"> <span class="block text-sm font-medium text-gray-900">
<i18n.Translate>using the payto:// format</i18n.Translate> <i18n.Translate>Import payto:// URI</i18n.Translate>
</span> </span>
</span> </span>
</span> </span>
@ -195,7 +220,7 @@ export function PaytoWireTransferForm({
</div> </div>
<form <form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 w-fit mx-auto"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
onSubmit={e => { onSubmit={e => {
@ -203,105 +228,106 @@ export function PaytoWireTransferForm({
}} }}
> >
<div class="px-4 py-6 sm:p-8"> <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"> {!isRawPayto ?
{!isRawPayto ? <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<Fragment>
<div class="sm:col-span-5"> <div class="sm:col-span-5">
<label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Account number`}</label> <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</label>
<div class="mt-2"> <div class="mt-2">
<input <input
ref={ref} ref={focus ? doAutoFocus : undefined}
type="text" 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 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" name="iban"
id="iban" id="iban"
value={iban ?? ""} value={iban ?? ""}
placeholder="CC0123456789" placeholder="CC0123456789"
autocomplete="off" autocomplete="off"
required required
pattern={ibanRegex} pattern={ibanRegex}
onInput={(e): void => { onInput={(e): void => {
setIban(e.currentTarget.value); setIban(e.currentTarget.value.toUpperCase());
}} }}
/> />
<ShowInputErrorLabel <ShowInputErrorLabel
message={errorsWire?.iban} message={errorsWire?.iban}
isDirty={iban !== undefined} isDirty={iban !== undefined}
/> />
</div>
<p class="mt-2 text-sm text-gray-500" >the receiver of the money</p>
</div> </div>
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate>IBAN of the recipient's account</i18n.Translate>
</p>
</div>
<div class="sm:col-span-5"> <div class="sm:col-span-5">
<label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label> <label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label>
<div class="mt-2"> <div class="mt-2">
<input <input
type="text" 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 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="subject" name="subject"
id="subject" id="subject"
autocomplete="off" autocomplete="off"
placeholder="subject" placeholder="subject"
value={subject ?? ""} value={subject ?? ""}
required required
onInput={(e): void => { onInput={(e): void => {
setSubject(e.currentTarget.value); setSubject(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errorsWire?.subject}
isDirty={subject !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p>
</div>
<div class="sm:col-span-5">
<label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
<Amount
name="amount"
currency={limit.currency}
value={trimmedAmountStr}
onChange={(d) => {
setAmount(d)
}} }}
/> />
<ShowInputErrorLabel <ShowInputErrorLabel
message={errorsWire?.subject} message={errorsWire?.subject}
isDirty={subject !== undefined} isDirty={subject !== undefined}
/> />
<p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
</div> </div>
<p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p>
</div>
</Fragment> : <div class="sm:col-span-5">
<Fragment> <label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
<div class="sm:col-span-6"> <Amount
<label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label> name="amount"
<div class="mt-2"> left
<input currency={limit.currency}
name="address" value={trimmedAmountStr}
id="address" onChange={(d) => {
type="text" setAmount(d)
size={50} }}
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" ref={ref} />
value={rawPaytoInput ?? ""} <ShowInputErrorLabel
required message={errorsWire?.amount}
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`} isDirty={subject !== undefined}
onInput={(e): void => { />
rawPaytoInputSetter(e.currentTarget.value); <p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
}} </div>
/>
<ShowInputErrorLabel </div> :
message={errorsPayto?.rawPaytoInput} <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
isDirty={rawPaytoInput !== undefined} <div class="sm:col-span-6">
/> <label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label>
</div> <div class="mt-2">
<textarea
ref={focus ? doAutoFocus : undefined}
name="address"
id="address"
type="textarea"
rows={3}
class="block overflow-hidden w-64 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"
value={rawPaytoInput ?? ""}
required
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
onInput={(e): void => {
rawPaytoInputSetter(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errorsPayto?.rawPaytoInput}
isDirty={rawPaytoInput !== undefined}
/>
</div> </div>
</div>
</Fragment> </div>
} }
</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"> <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
{onCancel ? {onCancel ?
@ -328,17 +354,37 @@ export function PaytoWireTransferForm({
) )
} }
/**
* Show the element when the load ended
* @param element
*/
export function doAutoFocus(element: HTMLElement | null) {
if (element) {
window.requestIdleCallback(() => {
element.focus()
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
})
})
}
}
export function Amount( export function Amount(
{ {
currency, currency,
name, name,
value, value,
error, error,
left,
onChange, onChange,
}: { }: {
error?: string; error?: string;
currency: string; currency: string;
name: string; name: string;
left?: boolean | undefined,
value: string | undefined; value: string | undefined;
onChange?: (s: string) => void; onChange?: (s: string) => void;
}, },
@ -346,13 +392,16 @@ export function Amount(
): VNode { ): VNode {
return ( return (
<div class="mt-2"> <div class="mt-2">
<div class="relative rounded-md shadow-sm"> <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<div class="pointer-events-none absolute inset-y-0 flex items-center pl-3"> <div
class="pointer-events-none inset-y-0 flex items-center px-3"
>
<span class="text-gray-500 sm:text-sm">{currency}</span> <span class="text-gray-500 sm:text-sm">{currency}</span>
</div> </div>
<input <input
type="number" type="number"
class="text-right block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 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" data-left={left}
class="text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
placeholder="0.00" aria-describedby="price-currency" placeholder="0.00" aria-describedby="price-currency"
ref={ref} ref={ref}
name={name} name={name}
@ -371,3 +420,4 @@ export function Amount(
</div> </div>
); );
} }

View File

@ -86,7 +86,6 @@ export function QrCodeSection({
</h3> </h3>
<div class="mt-4"> <div class="mt-4">
<a href={talerWithdrawUri} <a href={talerWithdrawUri}
// class="text-sm font-semibold leading-6 text-gray-900 btn "
class="inline-flex items-center 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" class="inline-flex items-center 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"
> >
<i18n.Translate>Click here to start</i18n.Translate> <i18n.Translate>Click here to start</i18n.Translate>

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
export function UpdateAccountPassword({ export function UpdateAccountPassword({
account, account,
@ -27,11 +28,6 @@ export function UpdateAccountPassword({
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
if (!result.ok) { if (!result.ok) {
if (result.loading || result.type === ErrorType.TIMEOUT) { if (result.loading || result.type === ErrorType.TIMEOUT) {
return onLoadNotOk(result); return onLoadNotOk(result);
@ -96,7 +92,7 @@ export function UpdateAccountPassword({
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input
ref={ref} ref={focus ? doAutoFocus : undefined}
type="password" 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" 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" name="password"

View File

@ -29,14 +29,15 @@ import {
notifyError, notifyError,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/browser"; } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact"; import { Fragment, VNode, h } from "preact";
import { forwardRef } from "preact/compat"; import { forwardRef } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { useAccessAPI } from "../hooks/access.js"; import { useAccessAPI } from "../hooks/access.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { Amount } from "./PaytoWireTransferForm.js"; import { Amount, doAutoFocus } from "./PaytoWireTransferForm.js";
import { useSettings } from "../hooks/settings.js"; import { useSettings } from "../hooks/settings.js";
import { OperationState } from "./OperationState/index.js"; import { OperationState } from "./OperationState/index.js";
import { Attention } from "../components/Attention.js";
const logger = new Logger("WalletWithdrawForm"); const logger = new Logger("WalletWithdrawForm");
const RefAmount = forwardRef(Amount); const RefAmount = forwardRef(Amount);
@ -53,47 +54,13 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
const { createWithdrawal } = useAccessAPI(); const { createWithdrawal } = useAccessAPI();
const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`); const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
if (!!settings.currentWithdrawalOperationId) { if (!!settings.currentWithdrawalOperationId) {
return <div> return <Attention type="warning" title={i18n.str`There is an operation already`}>
<i18n.Translate>
<div class="rounded-md bg-yellow-50 ring-yellow-2 p-4"> To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
<div class="flex"> </i18n.Translate>
<div class="flex-shrink-0"> </Attention>
<svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-800">
<i18n.Translate>There is an operation already</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>
To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
</i18n.Translate>
</p>
</div>
</div>
</div>
</div >
<div class="flex justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8 " >
<button type="button" class="text-sm font-semibold leading-6 text-gray-900 bg-white p-2 rounded-sm"
onClick={() => {
updateSettings("currentWithdrawalOperationId", undefined)
onCancel()
}}
>
<i18n.Translate>Cancel</i18n.Translate>
</button>
</div>
</div>
} }
const trimmedAmountStr = amountStr?.trim(); const trimmedAmountStr = amountStr?.trim();
@ -157,8 +124,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
e.preventDefault() e.preventDefault()
}} }}
> >
<div class="px-4 py-6 sm:p-8"> <div class="px-4 py-6 ">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> <div class="grid max-w-xs 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 for="withdraw-amount">{i18n.str`Amount`}</label> <label for="withdraw-amount">{i18n.str`Amount`}</label>
<RefAmount <RefAmount
@ -169,51 +136,53 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
setAmountStr(v); setAmountStr(v);
}} }}
error={errors?.amount} error={errors?.amount}
ref={ref} ref={focus ? doAutoFocus : undefined}
/> />
</div> </div>
<div class="sm:col-span-5"> </div>
<span class="isolate inline-flex rounded-md shadow-sm"> <div class="mt-4">
<button type="button" <div class="sm:inline">
class="relative inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
onClick={(e) => {
e.preventDefault();
setAmountStr("50.00")
}}
>
50.00
</button>
<button type="button"
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
onClick={(e) => {
e.preventDefault();
setAmountStr("25.00")
}}
>
25.00 <button type="button"
</button> class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
<button type="button" onClick={(e) => {
class="relative -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" e.preventDefault();
onClick={(e) => { setAmountStr("50.00")
e.preventDefault(); }}
setAmountStr("10.00") >
}} 50.00
> </button>
10.00 <button type="button"
</button> class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
<button type="button" onClick={(e) => {
class="relative inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" e.preventDefault();
onClick={(e) => { setAmountStr("25.00")
e.preventDefault(); }}
setAmountStr("5.00") >
}}
> 25.00
5.00 </button>
</button> </div>
</span> <div class="mt-4 sm:inline">
<button type="button"
class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
onClick={(e) => {
e.preventDefault();
setAmountStr("10.00")
}}
>
10.00
</button>
<button type="button"
class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
onClick={(e) => {
e.preventDefault();
setAmountStr("5.00")
}}
>
5.00
</button>
</div> </div>
</div> </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"> <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
@ -255,46 +224,20 @@ export function WalletWithdrawForm({
<div class="px-4 sm:px-0"> <div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2> <h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
<i18n.Translate>After using your wallet you will confirm or cancel the operation.</i18n.Translate> <i18n.Translate>After using your wallet you will need to confirm or cancel the operation on this site.</i18n.Translate>
</p> </p>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
{settings.showInstallWallet && <div class="rounded-md bg-blue-50 ring-blue-2 ring-2 p-4"> {settings.showInstallWallet &&
<div class="flex"> <Attention title={i18n.str`You need a GNU Taler Wallet`} onClose={() => {
<div class="flex-shrink-0"> updateSettings("showInstallWallet", false);
<svg class="h-5 w-5 text-blue-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> }}>
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> <i18n.Translate>
</svg> If you don't have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a>
</div> </i18n.Translate>
<div class="ml-3"> </Attention>
<h3 class="text-sm font-bold text-blue-800"> }
<i18n.Translate>You need a GNU Taler Wallet</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-blue-700">
<p>
<i18n.Translate>
If you dont have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a>
</i18n.Translate>
</p>
<p class="mt-3 text-sm flex justify-end">
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={(e) => {
e.preventDefault();
updateSettings("showInstallWallet", false);
}}
>
I know
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</p>
</div>
</div>
</div>
</div>}
{!settings.fastWithdrawal ? {!settings.fastWithdrawal ?
<OldWithdrawalForm <OldWithdrawalForm

View File

@ -37,6 +37,7 @@ import { useMemo, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js"; import { useAccessAnonAPI } from "../hooks/access.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { useSettings } from "../hooks/settings.js";
const logger = new Logger("WithdrawalConfirmationQuestion"); const logger = new Logger("WithdrawalConfirmationQuestion");
@ -59,6 +60,7 @@ export function WithdrawalConfirmationQuestion({
withdrawUri, withdrawUri,
}: Props): VNode { }: Props): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings()
const captchaNumbers = useMemo(() => { const captchaNumbers = useMemo(() => {
return { return {
@ -87,7 +89,9 @@ export function WithdrawalConfirmationQuestion({
await confirmWithdrawal( await confirmWithdrawal(
withdrawUri.withdrawalOperationId, withdrawUri.withdrawalOperationId,
); );
notifyInfo(i18n.str`Wire transfer completed!`) if (!settings.showWithdrawalSuccess) {
notifyInfo(i18n.str`Wire transfer completed!`)
}
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
notify( notify(
@ -203,7 +207,7 @@ export function WithdrawalConfirmationQuestion({
<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="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"> <div class="px-4 sm:px-0">
<h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer</i18n.Translate></h2> <h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</i18n.Translate></h2>
</div> </div>
<form <form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
@ -311,14 +315,10 @@ export function WithdrawalConfirmationQuestion({
} }
})()} })()}
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0 break-words">{details.reserve}</dd>
</div>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{Amounts.stringifyValue(details.amount)} {Amounts.currencyOf(details.amount)} {Amounts.stringifyValue(details.amount)}
</dd> </dd>
</div> </div>
</dl> </dl>

View File

@ -94,20 +94,12 @@ export function WithdrawalQRCode({
</div> </div>
<div class="mt-3 text-center sm:mt-5"> <div class="mt-3 text-center sm:mt-5">
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
<i18n.Translate>Withdrawal OK</i18n.Translate> <i18n.Translate>Withdrawal confirmed</i18n.Translate>
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<i18n.Translate> <i18n.Translate>
The wire transfer to the Taler exchange bank's account is completed, now the The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate>
</p>
</div>
<div class="mt-2">
<p >
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate> </i18n.Translate>
</p> </p>
</div> </div>

View File

@ -4,6 +4,7 @@ import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX = const EMAIL_REGEX =
@ -37,10 +38,6 @@ export function AccountForm({
RecursivePartial<typeof initial> | undefined RecursivePartial<typeof initial> | undefined
>(undefined); >(undefined);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
function updateForm(newForm: typeof initial): void { function updateForm(newForm: typeof initial): void {
@ -97,7 +94,6 @@ export function AccountForm({
<div class="px-4 py-6 sm:p-8"> <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="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 <label
class="block text-sm font-medium leading-6 text-gray-900" class="block text-sm font-medium leading-6 text-gray-900"
@ -108,7 +104,7 @@ export function AccountForm({
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input
ref={ref} ref={focus ? doAutoFocus : undefined}
type="text" 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" 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" name="username"

View File

@ -10,6 +10,7 @@ import { AdminAccount } from "./Account.js";
import { AccountList } from "./AccountList.js"; import { AccountList } from "./AccountList.js";
import { CreateNewAccount } from "./CreateNewAccount.js"; import { CreateNewAccount } from "./CreateNewAccount.js";
import { RemoveAccount } from "./RemoveAccount.js"; import { RemoveAccount } from "./RemoveAccount.js";
import { Transactions } from "../../components/Transactions/index.js";
/** /**
* Query account information and show QR code if there is pending withdrawal * Query account information and show QR code if there is pending withdrawal
@ -141,6 +142,7 @@ export function AdminHome({ onRegister }: Props): VNode {
<AdminAccount onRegister={onRegister} /> <AdminAccount onRegister={onRegister} />
<Transactions account="admin"/>
</Fragment> </Fragment>
); );
} }

View File

@ -6,6 +6,8 @@ import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util
import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { Attention } from "../../components/Attention.js";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
export function RemoveAccount({ export function RemoveAccount({
account, account,
@ -36,47 +38,15 @@ export function RemoveAccount({
} }
return onLoadNotOk(result); return onLoadNotOk(result);
} }
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
const balance = Amounts.parse(result.data.balance.amount); const balance = Amounts.parse(result.data.balance.amount);
if (!balance) { if (!balance) {
return <div>there was an error reading the balance</div>; return <div>there was an error reading the balance</div>;
} }
const isBalanceEmpty = Amounts.isZero(balance); const isBalanceEmpty = Amounts.isZero(balance);
if (!isBalanceEmpty) { if (!isBalanceEmpty) {
return <div> return <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}>
<div class="rounded-md bg-yellow-50 p-4"> <i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
<div class="flex"> </Attention>
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
<i18n.Translate>Can't delete the account</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
</p>
</div>
</div>
</div>
</div>
<div class="mt-2 flex justify-end">
<button type="button" class="rounded-md ring-1 ring-gray-400 bg-white px-3 py-2 text-sm font-semibold shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
onClick={() => {
onCancel()
}}>
<i18n.Translate>Go back</i18n.Translate>
</button>
</div>
</div>
} }
async function doRemove() { async function doRemove() {
@ -117,26 +87,9 @@ export function RemoveAccount({
return ( return (
<div> <div>
<div class="rounded-md bg-yellow-50 p-4"> <Attention type="warning" title={i18n.str`You are going to remove the account`}>
<div class="flex"> <i18n.Translate>This step can't be undone.</i18n.Translate>
<div class="flex-shrink-0"> </Attention>
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-800">
<i18n.Translate>You are going to remove the account</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>This step can't be undone.</i18n.Translate>
</p>
</div>
</div>
</div>
</div>
<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="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"> <div class="px-4 sm:px-0">
@ -164,7 +117,7 @@ export function RemoveAccount({
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input
ref={ref} ref={focus ? doAutoFocus : undefined}
type="text" 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 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" 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" name="password"