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 { 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({
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 { 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 {
const { i18n } = useTranslationContext()
return (
<div><div class="rounded-md bg-red-50 p-4">
<div class="flex">
<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">
return (<Attention type="danger" title={error.message as TranslatedString}>
<p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
</div>
</div>
</div>
</Attention>
);
}

View File

@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
import { format, isToday } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
import { useEffect, useRef } from "preact/hooks";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@ -55,9 +56,9 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<thead>
<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 lg:table-cell">{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="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="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="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 ">{i18n.str`Subject`}</th>
</tr>
</thead>
<tbody>
@ -69,22 +70,38 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
</th>
</tr>
{txs.map(item => {
return (<tr key={idx}>
<td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">{item.when.t_ms === "never"
? ""
: format(item.when.t_ms, "HH:mm:ss")}</div>
</td>
<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">
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>
)}</td>
<td class="px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
)}
</Fragment>
return (<tr key={idx}>
<td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">{time}</div>
<dl class="font-normal sm:hidden">
<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 data-negative={item.negative ? "true" : "false"}
class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
{amount}
</td>
<td class="hidden sm:table-cell 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>
</tr>)
})}

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

View File

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

View File

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

View File

@ -16,10 +16,10 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Attention } from "../../components/Attention.js";
import { Transactions } from "../../components/Transactions/index.js";
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
import { useSettings } from "../../hooks/settings.js";
import { bankUiSettings } from "../../settings.js";
import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
@ -31,23 +31,13 @@ export function InvalidIbanView({ error }: State.InvalidIban) {
const IS_PUBLIC_ACCOUNT_ENABLED = false
function ShowDemoInfo(): VNode {
const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings();
if (!settings.showDemoDescription) return <Fragment />
return <div class="rounded-md bg-blue-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" 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-blue-800">
<i18n.Translate>This is a demo bank!</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-blue-700">
return <Attention title={i18n.str`This is a demo bank`} onClose={() => {
updateSettings("showDemoDescription", false);
}}>
{IS_PUBLIC_ACCOUNT_ENABLED ? (
<i18n.Translate>
This part of the demo shows how a bank that supports Taler
@ -61,23 +51,7 @@ function ShowDemoInfo(): VNode {
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>
</Attention>
}
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 { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
import { NotificationMessage, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks";
import { LangSelector } from "../components/LangSelector.js";
@ -26,6 +26,7 @@ import { useSettings } from "../hooks/settings.js";
import { CopyButton, CopyIcon } from "../components/CopyButton.js";
import logo from "../assets/logo-2021.svg";
import { useAccountDetails } from "../hooks/access.js";
import { Attention } from "../components/Attention.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@ -108,7 +109,7 @@ export function BankFrame({
setOpen(!open)
}}>
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
@ -227,6 +228,22 @@ export function BankFrame({
</button>
</div>
</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">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
@ -286,10 +303,10 @@ export function BankFrame({
}
</div >
<StatusBanner />
<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="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
<StatusBanner />
{children}
</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 {
const notifs = useNotifications()
return <div
class="fixed top-10 z-20 ml-4 mr-4"
> {
if (notifs.length === 0) return <Fragment />
return <div class="fixed z-20 w-full p-4"> {
notifs.map(n => {
switch (n.message.type) {
case "error":
return <div class="rounded-md bg-red-50 p-4">
<div class="flex">
<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();
return <Attention type="danger" title={n.message.title} onClose={() => {
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 &&
<div class="mt-2 text-sm text-red-700">
{n.message.description}
</div>
}
<MaybeShowDebugInfo info={n.message.debug} />
{/* <a href="#" class="text-gray-500">
show debug info
</a>
{n.message.debug &&
<div class="mt-2 text-sm text-red-700 font-mono break-all">
{n.message.debug}
</div>
}
</div>
} */}
</Attention>
case "info":
return <div class="rounded-md bg-green-50 border-4 border-green-600 p-6">
<div class="flex">
<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();
return <Attention type="success" title={n.message.title} onClose={() => {
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>

View File

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

View File

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

View File

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

View File

@ -267,13 +267,12 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) {
</div>
<div class="mt-3 text-center sm:mt-5">
<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>
<div class="mt-2">
<p class="text-sm text-gray-500">
<i18n.Translate>
The wire transfer to the Taler exchange bank's account is completed, now the
exchange will send the requested amount into your GNU Taler wallet.
The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
</i18n.Translate>
</p>
</div>

View File

@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
const { i18n } = useTranslationContext();
const [settings] = useSettings();
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer");
return (
<div class="mt-2">
@ -82,7 +82,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
<i18n.Translate>another bank account</i18n.Translate>
</span>
<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>
@ -108,6 +108,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
limit={limit}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
setTab(undefined)
}}
onCancel={() => {
setTab(undefined)

View File

@ -55,10 +55,11 @@ export function PaytoWireTransferForm({
onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
const [iban, setIban] = useState<string | undefined>(undefined);
const [subject, setSubject] = useState<string | undefined>(undefined);
const [amount, setAmount] = useState<string | undefined>(undefined);
const [isRawPayto, setIsRawPayto] = useState(true);
// FIXME: remove this
const [iban, setIban] = useState<string | undefined>("DE4745461198061");
const [subject, setSubject] = useState<string | undefined>("ASD");
const [amount, setAmount] = useState<string | undefined>("1.00001");
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
@ -76,17 +77,17 @@ export function PaytoWireTransferForm({
const errorsWire = undefinedIfEmpty({
iban: !iban
? i18n.str`Missing IBAN`
? i18n.str`required`
: !IBAN_REGEX.test(iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
: validateIBAN(iban, i18n),
subject: !subject ? i18n.str`Missing subject` : undefined,
subject: !subject ? i18n.str`required` : undefined,
amount: !trimmedAmountStr
? i18n.str`Missing amount`
? i18n.str`required`
: !parsedAmount
? i18n.str`Amount is not valid`
? i18n.str`not valid`
: Amounts.isZero(parsedAmount)
? i18n.str`Should be greater than 0`
? i18n.str`should be greater than 0`
: Amounts.cmp(limit, parsedAmount) === -1
? i18n.str`balance is not enough`
: undefined,
@ -101,14 +102,14 @@ export function PaytoWireTransferForm({
? i18n.str`required`
: !parsed
? i18n.str`does not follow the pattern`
: !parsed.isKnown || parsed.targetType !== "iban"
? i18n.str`only "IBAN" target are supported`
: !parsed.params.amount
? i18n.str`use the "amount" parameter to specify the amount to be transferred`
: Amounts.parse(parsed.params.amount) === undefined
? i18n.str`the amount is not valid`
: !parsed.params.message
? i18n.str`use the "message" parameter to specify a reference text for the transfer`
: !parsed.isKnown || parsed.targetType !== "iban"
? i18n.str`only "IBAN" target are supported`
: !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
: validateIBAN(parsed.iban, i18n),
@ -159,6 +160,9 @@ export function PaytoWireTransferForm({
}
return (<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">
<h2 class="text-base font-semibold leading-7 text-gray-900">
{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">
<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={() => {
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)
}} />
<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")}>
<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)
}} />
<span class="flex flex-1">
<span class="flex flex-col">
<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>
@ -195,7 +220,7 @@ export function PaytoWireTransferForm({
</div>
<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"
autoCorrect="off"
onSubmit={e => {
@ -203,15 +228,14 @@ export function PaytoWireTransferForm({
}}
>
<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 ?
<Fragment>
<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">
<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">
<input
ref={ref}
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"
name="iban"
@ -222,7 +246,7 @@ export function PaytoWireTransferForm({
required
pattern={ibanRegex}
onInput={(e): void => {
setIban(e.currentTarget.value);
setIban(e.currentTarget.value.toUpperCase());
}}
/>
<ShowInputErrorLabel
@ -230,7 +254,9 @@ export function PaytoWireTransferForm({
isDirty={iban !== undefined}
/>
</div>
<p class="mt-2 text-sm text-gray-500" >the receiver of the money</p>
<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">
@ -261,6 +287,7 @@ export function PaytoWireTransferForm({
<label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
<Amount
name="amount"
left
currency={limit.currency}
value={trimmedAmountStr}
onChange={(d) => {
@ -268,23 +295,24 @@ export function PaytoWireTransferForm({
}}
/>
<ShowInputErrorLabel
message={errorsWire?.subject}
message={errorsWire?.amount}
isDirty={subject !== undefined}
/>
<p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
</div>
</Fragment> :
<Fragment>
</div> :
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
<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 class="mt-2">
<input
<textarea
ref={focus ? doAutoFocus : undefined}
name="address"
id="address"
type="text"
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}
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]`}
@ -298,10 +326,8 @@ export function PaytoWireTransferForm({
/>
</div>
</div>
</Fragment>
}
</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 ?
@ -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(
{
currency,
name,
value,
error,
left,
onChange,
}: {
error?: string;
currency: string;
name: string;
left?: boolean | undefined,
value: string | undefined;
onChange?: (s: string) => void;
},
@ -346,13 +392,16 @@ export function Amount(
): VNode {
return (
<div class="mt-2">
<div class="relative rounded-md shadow-sm">
<div class="pointer-events-none absolute inset-y-0 flex items-center pl-3">
<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 inset-y-0 flex items-center px-3"
>
<span class="text-gray-500 sm:text-sm">{currency}</span>
</div>
<input
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"
ref={ref}
name={name}
@ -371,3 +420,4 @@ export function Amount(
</div>
);
}

View File

@ -86,7 +86,6 @@ export function QrCodeSection({
</h3>
<div class="mt-4">
<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"
>
<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 { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
export function UpdateAccountPassword({
account,
@ -27,11 +28,6 @@ export function UpdateAccountPassword({
const [password, setPassword] = 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.loading || result.type === ErrorType.TIMEOUT) {
return onLoadNotOk(result);
@ -96,7 +92,7 @@ export function UpdateAccountPassword({
</label>
<div class="mt-2">
<input
ref={ref}
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"

View File

@ -29,14 +29,15 @@ import {
notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { Fragment, VNode, h } from "preact";
import { forwardRef } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
import { useAccessAPI } from "../hooks/access.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 { OperationState } from "./OperationState/index.js";
import { Attention } from "../components/Attention.js";
const logger = new Logger("WalletWithdrawForm");
const RefAmount = forwardRef(Amount);
@ -53,47 +54,13 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
const { createWithdrawal } = useAccessAPI();
const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
if (!!settings.currentWithdrawalOperationId) {
return <div>
<div class="rounded-md bg-yellow-50 ring-yellow-2 p-4">
<div class="flex">
<div class="flex-shrink-0">
<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>
return <Attention type="warning" title={i18n.str`There is an operation already`}>
<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>
</Attention>
}
const trimmedAmountStr = amountStr?.trim();
@ -157,8 +124,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
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="px-4 py-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">
<label for="withdraw-amount">{i18n.str`Amount`}</label>
<RefAmount
@ -169,13 +136,15 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
setAmountStr(v);
}}
error={errors?.amount}
ref={ref}
ref={focus ? doAutoFocus : undefined}
/>
</div>
<div class="sm:col-span-5">
<span class="isolate inline-flex rounded-md shadow-sm">
</div>
<div class="mt-4">
<div class="sm:inline">
<button type="button"
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"
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"
onClick={(e) => {
e.preventDefault();
setAmountStr("50.00")
@ -184,7 +153,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
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"
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"
onClick={(e) => {
e.preventDefault();
setAmountStr("25.00")
@ -193,8 +162,10 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
25.00
</button>
</div>
<div class="mt-4 sm:inline">
<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"
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")
@ -203,7 +174,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
10.00
</button>
<button type="button"
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"
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")
@ -211,9 +182,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
>
5.00
</button>
</span>
</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">
@ -255,46 +224,20 @@ export function WalletWithdrawForm({
<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>
<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>
</div>
<div class="col-span-2">
{settings.showInstallWallet && <div class="rounded-md bg-blue-50 ring-blue-2 ring-2 p-4">
<div class="flex">
<div class="flex-shrink-0">
<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" />
</svg>
</div>
<div class="ml-3">
<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();
{settings.showInstallWallet &&
<Attention title={i18n.str`You need a GNU Taler Wallet`} onClose={() => {
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>}
}}>
<i18n.Translate>
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>
</i18n.Translate>
</Attention>
}
{!settings.fastWithdrawal ?
<OldWithdrawalForm

View File

@ -37,6 +37,7 @@ import { useMemo, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { useSettings } from "../hooks/settings.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
@ -59,6 +60,7 @@ export function WithdrawalConfirmationQuestion({
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings()
const captchaNumbers = useMemo(() => {
return {
@ -87,7 +89,9 @@ export function WithdrawalConfirmationQuestion({
await confirmWithdrawal(
withdrawUri.withdrawalOperationId,
);
if (!settings.showWithdrawalSuccess) {
notifyInfo(i18n.str`Wire transfer completed!`)
}
} catch (error) {
if (error instanceof RequestError) {
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="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>
<form
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">
<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">
{Amounts.stringifyValue(details.amount)}
{Amounts.currencyOf(details.amount)} {Amounts.stringifyValue(details.amount)}
</dd>
</div>
</dl>

View File

@ -94,20 +94,12 @@ export function WithdrawalQRCode({
</div>
<div class="mt-3 text-center sm:mt-5">
<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>
<div class="mt-2">
<p class="text-sm text-gray-500">
<i18n.Translate>
The wire transfer to the Taler exchange bank's account is completed, now the
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate>
</p>
</div>
<div class="mt-2">
<p >
<i18n.Translate>
You can close this page now or continue to the account page.
The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
</i18n.Translate>
</p>
</div>

View File

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

View File

@ -10,6 +10,7 @@ import { AdminAccount } from "./Account.js";
import { AccountList } from "./AccountList.js";
import { CreateNewAccount } from "./CreateNewAccount.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
@ -141,6 +142,7 @@ export function AdminHome({ onRegister }: Props): VNode {
<AdminAccount onRegister={onRegister} />
<Transactions account="admin"/>
</Fragment>
);
}

View File

@ -6,6 +6,8 @@ import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util
import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
import { useEffect, useRef, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
import { Attention } from "../../components/Attention.js";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
export function RemoveAccount({
account,
@ -36,47 +38,15 @@ export function RemoveAccount({
}
return onLoadNotOk(result);
}
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
const balance = Amounts.parse(result.data.balance.amount);
if (!balance) {
return <div>there was an error reading the balance</div>;
}
const isBalanceEmpty = Amounts.isZero(balance);
if (!isBalanceEmpty) {
return <div>
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<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>
return <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}>
<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>
</Attention>
}
async function doRemove() {
@ -117,26 +87,9 @@ export function RemoveAccount({
return (
<div>
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<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-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>
<Attention type="warning" title={i18n.str`You are going to remove the account`}>
<i18n.Translate>This step can't be undone.</i18n.Translate>
</p>
</div>
</div>
</div>
</div>
</Attention>
<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">
@ -164,7 +117,7 @@ export function RemoveAccount({
</label>
<div class="mt-2">
<input
ref={ref}
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 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"