more ui
This commit is contained in:
parent
fdbe623e10
commit
e39d5c488e
9
packages/demobank-ui/src/assets/logo-2021.svg
Normal file
9
packages/demobank-ui/src/assets/logo-2021.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
|
||||||
|
<g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
|
||||||
|
<path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
|
||||||
|
<path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
|
||||||
|
<path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
|
||||||
|
</g>
|
||||||
|
<path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "left",
|
alignItems: "left",
|
||||||
@ -41,9 +40,7 @@ export function QR({ text }: { text: string }): VNode {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "50%",
|
width: "100%",
|
||||||
minWidth: 200,
|
|
||||||
maxWidth: 300,
|
|
||||||
marginRight: "auto",
|
marginRight: "auto",
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
}}
|
}}
|
||||||
|
@ -39,21 +39,21 @@ export function ReadyView({ transactions }: State.Ready): VNode {
|
|||||||
<h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>
|
<h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg">
|
<div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg min-w-fit bg-white">
|
||||||
<table class="min-w-full divide-y divide-gray-300">
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="pl-4 pr-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">{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="px-3 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`Amount`}</th>
|
||||||
<th scope="col" class="px-3 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`Counterpart`}</th>
|
||||||
<th scope="col" class="px-3 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 lg:table-cell">{i18n.str`Subject`}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{transactions.map((item, idx) => {
|
{transactions.map((item, idx) => {
|
||||||
return (
|
return (
|
||||||
<tr key={idx}>
|
<tr key={idx}>
|
||||||
<td class="relative py-4 pl-4 pr-3 text-sm sm:pl-6">
|
<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">{item.when.t_ms === "never"
|
||||||
? ""
|
? ""
|
||||||
: format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}</div>
|
: format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}</div>
|
||||||
@ -66,7 +66,7 @@ export function ReadyView({ transactions }: State.Ready): VNode {
|
|||||||
<span style={{ color: "grey" }}><{i18n.str`invalid value`}></span>
|
<span style={{ color: "grey" }}><{i18n.str`invalid value`}></span>
|
||||||
)}</td>
|
)}</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">{item.counterpart}</td>
|
||||||
<td class="px-3 py-3.5 text-sm text-gray-500">{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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
66
packages/demobank-ui/src/forms/simplest.ts
Normal file
66
packages/demobank-ui/src/forms/simplest.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AmountJson,
|
||||||
|
TranslatedString
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { DoubleColumnForm, FormState } from "@gnu-taler/web-util/browser";
|
||||||
|
|
||||||
|
export namespace Data {
|
||||||
|
export interface WithResolution {
|
||||||
|
when: AbsoluteTime;
|
||||||
|
threshold: AmountJson;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
export interface Form extends WithResolution {
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const design: DoubleColumnForm = [
|
||||||
|
{
|
||||||
|
title: "Simple form" as TranslatedString,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "textArea",
|
||||||
|
props: {
|
||||||
|
name: "comment",
|
||||||
|
label: "Comments" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Resolution" as TranslatedString,
|
||||||
|
description: `Current state is and threshold at ` as TranslatedString,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "date",
|
||||||
|
props: {
|
||||||
|
name: "when",
|
||||||
|
label: "Decision Time" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "amount",
|
||||||
|
props: {
|
||||||
|
name: "threshold",
|
||||||
|
label: "New threshold" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
,
|
||||||
|
];
|
||||||
|
|
||||||
|
function formBehavior(v: Partial<Data.Form>): FormState<Data.Form> {
|
||||||
|
return {
|
||||||
|
when: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
threshold: {
|
||||||
|
// disabled: v.state === AmlExchangeBackend.AmlState.frozen,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
|||||||
import { TranslatedString } from "@gnu-taler/taler-util";
|
|
||||||
import { memoryMap } from "@gnu-taler/web-util/browser";
|
|
||||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
export type NotificationMessage = ErrorNotification | InfoNotification;
|
|
||||||
|
|
||||||
//FIXME: this should not be exported since every notification
|
|
||||||
// goes throw notify function
|
|
||||||
export interface ErrorMessage {
|
|
||||||
description?: string;
|
|
||||||
title: TranslatedString;
|
|
||||||
debug?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ErrorNotification {
|
|
||||||
type: "error";
|
|
||||||
error: ErrorMessage;
|
|
||||||
}
|
|
||||||
interface InfoNotification {
|
|
||||||
type: "info";
|
|
||||||
info: TranslatedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage = memoryMap<NotificationMessage>();
|
|
||||||
const NOTIFICATION_KEY = "notification";
|
|
||||||
|
|
||||||
export function onNotificationUpdate(
|
|
||||||
handler: (newValue: NotificationMessage | undefined) => void,
|
|
||||||
) {
|
|
||||||
return storage.onUpdate(NOTIFICATION_KEY, () => {
|
|
||||||
const newValue = storage.get(NOTIFICATION_KEY);
|
|
||||||
handler(newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifyError(error: ErrorMessage) {
|
|
||||||
storage.set(NOTIFICATION_KEY, { type: "error", error });
|
|
||||||
}
|
|
||||||
export function notifyInfo(info: TranslatedString) {
|
|
||||||
storage.set(NOTIFICATION_KEY, { type: "info", info });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNotifications(): [
|
|
||||||
NotificationMessage | undefined,
|
|
||||||
StateUpdater<NotificationMessage | undefined>,
|
|
||||||
] {
|
|
||||||
const [value, setter] = useState<NotificationMessage | undefined>();
|
|
||||||
useEffect(() => {
|
|
||||||
return storage.onUpdate(NOTIFICATION_KEY, () => {
|
|
||||||
setter(storage.get(NOTIFICATION_KEY));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return [value, setter];
|
|
||||||
}
|
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
|
import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
|
||||||
import { AbsoluteTime, AmountJson, PaytoUriIBAN } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util";
|
||||||
import { Loading } from "../../components/Loading.js";
|
import { Loading } from "../../components/Loading.js";
|
||||||
import { useComponentState } from "./state.js";
|
import { useComponentState } from "./state.js";
|
||||||
import { ReadyView, InvalidIbanView} from "./views.js";
|
import { ReadyView, InvalidIbanView} from "./views.js";
|
||||||
@ -51,7 +51,7 @@ export namespace State {
|
|||||||
status: "ready";
|
status: "ready";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
account: string,
|
account: string,
|
||||||
payto: PaytoUriIBAN,
|
payto: PaytoUriIBAN | PaytoUriTalerBank,
|
||||||
balance: AmountJson,
|
balance: AmountJson,
|
||||||
balanceIsDebit: boolean,
|
balanceIsDebit: boolean,
|
||||||
limit: AmountJson,
|
limit: AmountJson,
|
||||||
|
@ -15,10 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
|
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { useBackendContext } from "../../context/backend.js";
|
import { useBackendContext } from "../../context/backend.js";
|
||||||
import { useAccountDetails } from "../../hooks/access.js";
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
import { notifyError } from "../../hooks/notification.js";
|
|
||||||
import { Props, State } from "./index.js";
|
import { Props, State } from "./index.js";
|
||||||
|
|
||||||
export function useComponentState({ account, onLoadNotOk }: Props): State {
|
export function useComponentState({ account, onLoadNotOk }: Props): State {
|
||||||
@ -43,9 +42,7 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
|
|||||||
//logout if there is any error, not if loading
|
//logout if there is any error, not if loading
|
||||||
backend.logOut();
|
backend.logOut();
|
||||||
if (result.status === HttpStatusCode.NotFound) {
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
notifyError({
|
notifyError(i18n.str`Username or account label "${account}" not found`, undefined);
|
||||||
title: i18n.str`Username or account label "${account}" not found`,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
status: "error-user-not-found",
|
status: "error-user-not-found",
|
||||||
error: result,
|
error: result,
|
||||||
@ -62,7 +59,8 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
|
|||||||
const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
|
const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
|
||||||
const payto = parsePaytoUri(data.paytoUri);
|
const payto = parsePaytoUri(data.paytoUri);
|
||||||
|
|
||||||
if (!payto || !payto.isKnown || payto.targetType !== "iban") {
|
if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
|
||||||
|
console.log(payto)
|
||||||
return {
|
return {
|
||||||
status: "invalid-iban",
|
status: "invalid-iban",
|
||||||
error: result
|
error: result
|
||||||
|
@ -14,11 +14,14 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
|
import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
ErrorType,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
|
notifyInfo,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
@ -39,12 +42,10 @@ import {
|
|||||||
validateIBAN,
|
validateIBAN,
|
||||||
WithIntermediate,
|
WithIntermediate,
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
|
||||||
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
import { ShowCashoutDetails } from "./BusinessAccount.js";
|
||||||
import { handleNotOkResult } from "./HomePage.js";
|
import { handleNotOkResult } from "./HomePage.js";
|
||||||
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
|
|
||||||
|
|
||||||
const charset =
|
const charset =
|
||||||
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
@ -362,6 +363,7 @@ function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
notifyInfo(i18n.str`Wire transfer created!`);
|
notifyInfo(i18n.str`Wire transfer created!`);
|
||||||
}}
|
}}
|
||||||
|
onCancel={undefined}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -414,7 +416,6 @@ export function UpdateAccountPassword({
|
|||||||
const { changePassword } = useAdminAccountAPI();
|
const { changePassword } = useAdminAccountAPI();
|
||||||
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 [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
@ -431,8 +432,8 @@ export function UpdateAccountPassword({
|
|||||||
repeat: !repeat
|
repeat: !repeat
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: password !== repeat
|
: password !== repeat
|
||||||
? i18n.str`password doesn't match`
|
? i18n.str`password doesn't match`
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -442,9 +443,6 @@ export function UpdateAccountPassword({
|
|||||||
<i18n.Translate>Update password for {account}</i18n.Translate>
|
<i18n.Translate>Update password for {account}</i18n.Translate>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
@ -507,15 +505,11 @@ export function UpdateAccountPassword({
|
|||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(buildRequestErrorMessage(i18n, error.cause));
|
notify(buildRequestErrorMessage(i18n, error.cause));
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
|
||||||
title: i18n.str`Operation failed, please report`,
|
? error.message
|
||||||
description:
|
: JSON.stringify(error)) as TranslatedString)
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -540,7 +534,6 @@ function CreateNewAccount({
|
|||||||
const [submitAccount, setSubmitAccount] = useState<
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
SandboxBackend.Circuit.CircuitAccountData | undefined
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
>();
|
>();
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@ -548,9 +541,6 @@ function CreateNewAccount({
|
|||||||
<i18n.Translate>New account</i18n.Translate>
|
<i18n.Translate>New account</i18n.Translate>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
<AccountForm
|
<AccountForm
|
||||||
@ -587,39 +577,38 @@ function CreateNewAccount({
|
|||||||
if (!submitAccount) return;
|
if (!submitAccount) return;
|
||||||
try {
|
try {
|
||||||
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
||||||
{
|
{
|
||||||
cashout_address: submitAccount.cashout_address,
|
cashout_address: submitAccount.cashout_address,
|
||||||
contact_data: submitAccount.contact_data,
|
contact_data: submitAccount.contact_data,
|
||||||
internal_iban: submitAccount.iban,
|
internal_iban: submitAccount.iban,
|
||||||
name: submitAccount.name,
|
name: submitAccount.name,
|
||||||
username: submitAccount.username,
|
username: submitAccount.username,
|
||||||
password: randomPassword(),
|
password: randomPassword(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await createAccount(account);
|
await createAccount(account);
|
||||||
onCreateSuccess(account.password);
|
onCreateSuccess(account.password);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.Forbidden
|
status === HttpStatusCode.Forbidden
|
||||||
? i18n.str`The rights to perform the operation are not sufficient`
|
? i18n.str`The rights to perform the operation are not sufficient`
|
||||||
: status === HttpStatusCode.BadRequest
|
: status === HttpStatusCode.BadRequest
|
||||||
? i18n.str`Input data was invalid`
|
? i18n.str`Input data was invalid`
|
||||||
: status === HttpStatusCode.Conflict
|
: status === HttpStatusCode.Conflict
|
||||||
? i18n.str`At least one registration detail was not available`
|
? i18n.str`At least one registration detail was not available`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(
|
||||||
title: i18n.str`Operation failed, please report`,
|
i18n.str`Operation failed, please report`,
|
||||||
description:
|
(error instanceof Error
|
||||||
error instanceof Error
|
? error.message
|
||||||
? error.message
|
: JSON.stringify(error)) as TranslatedString
|
||||||
: JSON.stringify(error),
|
)
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -654,7 +643,6 @@ export function ShowAccountDetails({
|
|||||||
const [submitAccount, setSubmitAccount] = useState<
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
SandboxBackend.Circuit.CircuitAccountData | undefined
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
>();
|
>();
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
@ -673,9 +661,6 @@ export function ShowAccountDetails({
|
|||||||
<i18n.Translate>Business account details</i18n.Translate>
|
<i18n.Translate>Business account details</i18n.Translate>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
<AccountForm
|
<AccountForm
|
||||||
template={result.data}
|
template={result.data}
|
||||||
@ -740,24 +725,23 @@ export function ShowAccountDetails({
|
|||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.Forbidden
|
status === HttpStatusCode.Forbidden
|
||||||
? i18n.str`The rights to change the account are not sufficient`
|
? i18n.str`The rights to change the account are not sufficient`
|
||||||
: status === HttpStatusCode.NotFound
|
: status === HttpStatusCode.NotFound
|
||||||
? i18n.str`The username was not found`
|
? i18n.str`The username was not found`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(
|
||||||
title: i18n.str`Operation failed, please report`,
|
i18n.str`Operation failed, please report`,
|
||||||
description:
|
(error instanceof Error
|
||||||
error instanceof Error
|
? error.message
|
||||||
? error.message
|
: JSON.stringify(error)) as TranslatedString
|
||||||
: JSON.stringify(error),
|
)
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -788,7 +772,6 @@ function RemoveAccount({
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
const { deleteAccount } = useAdminAccountAPI();
|
const { deleteAccount } = useAdminAccountAPI();
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
@ -812,7 +795,8 @@ function RemoveAccount({
|
|||||||
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{!isBalanceEmpty && (
|
{/* {FXME: SHOW WARNING} */}
|
||||||
|
{/* {!isBalanceEmpty && (
|
||||||
<ErrorBannerFloat
|
<ErrorBannerFloat
|
||||||
error={{
|
error={{
|
||||||
title: i18n.str`Can't delete the account`,
|
title: i18n.str`Can't delete the account`,
|
||||||
@ -820,10 +804,7 @@ function RemoveAccount({
|
|||||||
}}
|
}}
|
||||||
onClear={() => saveError(undefined)}
|
onClear={() => saveError(undefined)}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
@ -852,26 +833,23 @@ function RemoveAccount({
|
|||||||
onUpdateSuccess();
|
onUpdateSuccess();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.Forbidden
|
status === HttpStatusCode.Forbidden
|
||||||
? i18n.str`The administrator specified a institutional username`
|
? i18n.str`The administrator specified a institutional username`
|
||||||
: status === HttpStatusCode.NotFound
|
: status === HttpStatusCode.NotFound
|
||||||
? i18n.str`The username was not found`
|
? i18n.str`The username was not found`
|
||||||
: status === HttpStatusCode.PreconditionFailed
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
? i18n.str`Balance was not zero`
|
? i18n.str`Balance was not zero`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(i18n.str`Operation failed, please report`,
|
||||||
title: i18n.str`Operation failed, please report`,
|
(error instanceof Error
|
||||||
description:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
? error.message
|
||||||
: JSON.stringify(error),
|
: JSON.stringify(error)) as TranslatedString);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -915,31 +893,31 @@ function AccountForm({
|
|||||||
cashout_address: !newForm.cashout_address
|
cashout_address: !newForm.cashout_address
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !parsed
|
: !parsed
|
||||||
? i18n.str`does not follow the pattern`
|
? i18n.str`does not follow the pattern`
|
||||||
: !parsed.isKnown || parsed.targetType !== "iban"
|
: !parsed.isKnown || parsed.targetType !== "iban"
|
||||||
? i18n.str`only "IBAN" target are supported`
|
? i18n.str`only "IBAN" target are supported`
|
||||||
: !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),
|
||||||
contact_data: undefinedIfEmpty({
|
contact_data: undefinedIfEmpty({
|
||||||
email: !newForm.contact_data?.email
|
email: !newForm.contact_data?.email
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
||||||
? i18n.str`it should be an email`
|
? i18n.str`it should be an email`
|
||||||
: undefined,
|
: undefined,
|
||||||
phone: !newForm.contact_data?.phone
|
phone: !newForm.contact_data?.phone
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !newForm.contact_data.phone.startsWith("+")
|
: !newForm.contact_data.phone.startsWith("+")
|
||||||
? i18n.str`should start with +`
|
? i18n.str`should start with +`
|
||||||
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
||||||
? i18n.str`phone number can't have other than numbers`
|
? i18n.str`phone number can't have other than numbers`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
iban: !newForm.iban
|
iban: !newForm.iban
|
||||||
? undefined //optional field
|
? undefined //optional field
|
||||||
: !IBAN_REGEX.test(newForm.iban)
|
: !IBAN_REGEX.test(newForm.iban)
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
: validateIBAN(newForm.iban, i18n),
|
: validateIBAN(newForm.iban, i18n),
|
||||||
name: !newForm.name ? i18n.str`required` : undefined,
|
name: !newForm.name ? i18n.str`required` : undefined,
|
||||||
username: !newForm.username ? i18n.str`required` : undefined,
|
username: !newForm.username ? i18n.str`required` : undefined,
|
||||||
});
|
});
|
||||||
|
@ -15,17 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
import { Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { 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, useState } from "preact/hooks";
|
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
||||||
import talerLogo from "../assets/logo-white.svg";
|
|
||||||
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
|
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { useBusinessAccountDetails } from "../hooks/circuit.js";
|
import { useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js";
|
|
||||||
import { CopyButton, CopyIcon } from "../components/CopyButton.js";
|
import { CopyButton, CopyIcon } from "../components/CopyButton.js";
|
||||||
|
import logo from "../assets/logo-2021.svg";
|
||||||
|
|
||||||
const IS_PUBLIC_ACCOUNT_ENABLED = false;
|
const IS_PUBLIC_ACCOUNT_ENABLED = false;
|
||||||
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
||||||
@ -81,16 +80,23 @@ export function BankFrame({
|
|||||||
</a>,
|
</a>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (<div class="min-h-full">
|
return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
|
||||||
<div class="bg-indigo-600 pb-32">
|
<div class="bg-indigo-600 pb-32">
|
||||||
<nav class="border-b border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-none">
|
<nav class="">
|
||||||
<div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
|
<div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
|
||||||
<div class="relative flex h-16 items-center justify-between lg:border-b lg:border-indigo-400 lg:border-opacity-25">
|
<div class="relative flex h-16 items-center justify-between ">
|
||||||
<div class="flex items-center px-2 lg:px-0">
|
<div class="flex items-center px-2 lg:px-0">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0 bg-white rounded-lg">
|
||||||
<img class="block h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company" />
|
<a href="#/">
|
||||||
|
<img
|
||||||
|
class="h-8 w-auto"
|
||||||
|
src={logo}
|
||||||
|
alt="Taler"
|
||||||
|
style={{ height: 35, margin: 10 }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden lg:ml-10 lg:block">
|
<div class="hidden sm:block lg:ml-10 ">
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
|
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
|
||||||
{bankUiSettings.demoSites.map(([name, url]) => {
|
{bankUiSettings.demoSites.map(([name, url]) => {
|
||||||
@ -100,62 +106,131 @@ export function BankFrame({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex lg:hidden">
|
<div class="flex">
|
||||||
{/* <!-- Mobile menu button --> */}
|
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"
|
||||||
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
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 main menu</span>
|
||||||
{/* <!-- Menu open: "hidden", Menu closed: "block" --> */}
|
<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-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 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>
|
||||||
{/* <!-- Menu open: "block", Menu closed: "hidden" --> */}
|
|
||||||
<svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <!-- Mobile menu, show/hide based on menu state. --> */}
|
|
||||||
{open &&
|
{open &&
|
||||||
<div class="lg:hidden" id="mobile-menu">
|
<Fragment>
|
||||||
<div class="space-y-1 px-2 pb-3 pt-2">
|
<div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true"
|
||||||
{/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
|
onClick={() => {
|
||||||
{bankUiSettings.demoSites.map(([name, url]) => {
|
setOpen(false)
|
||||||
return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md py-2 px-3 text-base font-medium">{name}</a>
|
}}>
|
||||||
})}
|
<div class="fixed inset-0"></div>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 overflow-hidden">
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||||
|
<div class="pointer-events-auto w-screen max-w-md" >
|
||||||
|
<div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => {
|
||||||
|
//do not trigger close if clicking inside the sidebar
|
||||||
|
e.stopPropagation();
|
||||||
|
}}>
|
||||||
|
<div class="px-4 sm:px-6" >
|
||||||
|
<div class="flex items-start justify-between" >
|
||||||
|
<h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title">
|
||||||
|
<i18n.Translate>Settings</i18n.Translate>
|
||||||
|
</h2>
|
||||||
|
<div class="ml-3 flex h-7 items-center">
|
||||||
|
<button type="button" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
<span class="absolute -inset-2.5"></span>
|
||||||
|
<span class="sr-only">
|
||||||
|
<i18n.Translate>Close panel</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative mt-6 flex-1 px-4 sm:px-6">
|
||||||
|
{/* <!-- Your content --> */}
|
||||||
|
|
||||||
|
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
|
||||||
|
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||||
|
<li>
|
||||||
|
<ul role="list" class="-mx-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<a href="#"
|
||||||
|
class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
|
||||||
|
onClick={() => {
|
||||||
|
backend.logOut();
|
||||||
|
setOpen(false)
|
||||||
|
updateSettings("currentWithdrawalOperationId", undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||||
|
</svg>
|
||||||
|
Log out
|
||||||
|
{/* <span class="ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-gray-50 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600 ring-1 ring-inset ring-gray-200" aria-hidden="true">5</span> */}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="sm:hidden">
|
||||||
|
<div class="text-xs font-semibold leading-6 text-gray-400">
|
||||||
|
<i18n.Translate>Sites</i18n.Translate>
|
||||||
|
</div>
|
||||||
|
<ul role="list" class="-mx-2 mt-2 space-y-1">
|
||||||
|
{bankUiSettings.demoSites.map(([name, url]) => {
|
||||||
|
return <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
|
||||||
|
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">></span>
|
||||||
|
<span class="truncate">{name}</span>
|
||||||
|
</a>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</nav >
|
</nav >
|
||||||
<header class="py-10">
|
|
||||||
|
|
||||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
{true &&
|
||||||
<div class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
|
<header class="py-5 border-t border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-t lg:border-indigo-400 lg:border-opacity-25">
|
||||||
{/* <h1 class="text-base font-semibold leading-6 text-gray-900"></h1> */}
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<h1 class="text-3xl font-bold tracking-tight text-white"><WelcomeAccount /></h1>
|
<div class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
|
||||||
<div>
|
<h3 class="text-2xl font-bold tracking-tight text-white"><WelcomeAccount /></h3>
|
||||||
|
<div>
|
||||||
<h2 class="text-3xl font-bold tracking-tight text-white">KUDOS 100.00</h2>
|
<h3 class="text-2xl font-bold tracking-tight text-white"><AccountBalance /></h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div class="ml-4 mt-2 flex-shrink-0">
|
|
||||||
<button type="button" class="relative inline-flex items-center 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">Create new job</button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
}
|
||||||
</div >
|
</div >
|
||||||
|
|
||||||
<main class="-mt-32">
|
<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">
|
||||||
{/* <!-- Your content --> */}
|
<StatusBanner />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -207,15 +282,15 @@ export function BankFrame({
|
|||||||
// />
|
// />
|
||||||
// ) : undefined}
|
// ) : undefined}
|
||||||
|
|
||||||
// <LangSelector />
|
// <LangSelector />
|
||||||
|
|
||||||
// <a
|
// <a
|
||||||
// href="#"
|
// href="#"
|
||||||
// class="pure-button logout-button"
|
// class="pure-button logout-button"
|
||||||
// onClick={() => {
|
// onClick={() => {
|
||||||
// backend.logOut();
|
// backend.logOut();
|
||||||
// updateSettings("currentWithdrawalOperationId", undefined);
|
// updateSettings("currentWithdrawalOperationId", undefined);
|
||||||
// }}
|
// }}
|
||||||
// >{i18n.str`Logout`}</a>
|
// >{i18n.str`Logout`}</a>
|
||||||
// </Fragment>
|
// </Fragment>
|
||||||
// ) : undefined}
|
// ) : undefined}
|
||||||
@ -225,149 +300,110 @@ export function BankFrame({
|
|||||||
// <StatusBanner />
|
// <StatusBanner />
|
||||||
// {children}
|
// {children}
|
||||||
// </section>
|
// </section>
|
||||||
// <section id="footer" class="footer">
|
|
||||||
// <hr />
|
|
||||||
// <div>
|
|
||||||
// <p>
|
|
||||||
// You can learn more about GNU Taler on our{" "}
|
|
||||||
// <a href="https://taler.net">main website</a>.
|
|
||||||
// </p>
|
|
||||||
// </div>
|
|
||||||
// <div style="flex-grow:1" />
|
|
||||||
// <p>
|
|
||||||
// Copyright © 2014—2022 Taler Systems SA. {versionText}{" "}
|
|
||||||
// <TestingTag />
|
|
||||||
// </p>
|
|
||||||
// </section>
|
|
||||||
// </Fragment>
|
// </Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeDemoContent(content: VNode): VNode {
|
// function maybeDemoContent(content: VNode): VNode {
|
||||||
if (bankUiSettings.showDemoNav) {
|
// if (bankUiSettings.showDemoNav) {
|
||||||
return content;
|
// return content;
|
||||||
}
|
// }
|
||||||
return <Fragment />;
|
// return <Fragment />;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function ErrorBannerFloat({
|
// export function ErrorBannerFloat({
|
||||||
error,
|
// error,
|
||||||
onClear,
|
// onClear,
|
||||||
}: {
|
// }: {
|
||||||
error: ErrorMessage;
|
// error: ErrorMessage;
|
||||||
onClear?: () => void;
|
// onClear?: () => void;
|
||||||
}): VNode {
|
// }): VNode {
|
||||||
return (
|
// return (
|
||||||
<div
|
// <div
|
||||||
style={{
|
// style={{
|
||||||
position: "fixed",
|
// position: "fixed",
|
||||||
top: 10,
|
// top: 10,
|
||||||
zIndex: 200,
|
// zIndex: 200,
|
||||||
width: "90%",
|
// width: "90%",
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
<ErrorBanner error={error} onClear={onClear} />
|
// <ErrorBanner error={error} onClear={onClear} />
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
function ErrorBanner({
|
function StatusBanner(): VNode {
|
||||||
error,
|
const notifs = useNotifications()
|
||||||
onClear,
|
return <div
|
||||||
}: {
|
class="fixed top-10 z-20 ml-4 mr-4"
|
||||||
error: ErrorMessage;
|
> {
|
||||||
onClear?: () => void;
|
notifs.map(n => {
|
||||||
}): VNode {
|
const info = n.message.type === "info" ? n.message : undefined
|
||||||
return (
|
const error = n.message.type === "error" ? n.message : undefined
|
||||||
<div
|
switch (n.message.type) {
|
||||||
class="informational informational-fail"
|
case "error":
|
||||||
style={{
|
return <div class="rounded-md bg-red-50 p-4">
|
||||||
marginTop: 8,
|
<div class="flex">
|
||||||
paddingLeft: 16,
|
<div class="flex-shrink-0">
|
||||||
paddingRight: 16,
|
<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 style={{ display: "flex", justifyContent: "space-between" }}>
|
</div>
|
||||||
<p>
|
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||||
<b>{error.title}</b>
|
<p class="text-sm font-medium text-red-800">{n.message.title}</p>
|
||||||
</p>
|
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
||||||
<div style={{ marginTop: "auto", marginBottom: "auto" }}>
|
<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"
|
||||||
{onClear && (
|
onClick={(e) => {
|
||||||
<input
|
e.preventDefault();
|
||||||
type="button"
|
n.remove()
|
||||||
class="pure-button"
|
}}
|
||||||
value="Clear"
|
>
|
||||||
onClick={(e) => {
|
Close
|
||||||
e.preventDefault();
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
onClear();
|
<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>
|
|
||||||
<p>{error.description}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBanner(): VNode | null {
|
|
||||||
const [info, setInfo] = useState<TranslatedString>();
|
|
||||||
const [error, setError] = useState<ErrorMessage>();
|
|
||||||
useEffect(() => {
|
|
||||||
return onNotificationUpdate((newValue) => {
|
|
||||||
if (newValue === undefined) {
|
|
||||||
setInfo(undefined);
|
|
||||||
setError(undefined);
|
|
||||||
} else {
|
|
||||||
if (newValue.type === "error") {
|
|
||||||
setError(newValue.error);
|
|
||||||
} else {
|
|
||||||
setInfo(newValue.info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 10,
|
|
||||||
zIndex: 200,
|
|
||||||
width: "90%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!info ? undefined : (
|
|
||||||
<div
|
|
||||||
class="informational informational-ok"
|
|
||||||
style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
||||||
<p>
|
|
||||||
<b>{info}</b>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="button"
|
|
||||||
class="pure-button"
|
|
||||||
value="Clear"
|
|
||||||
onClick={async () => {
|
|
||||||
setInfo(undefined);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{n.message.description &&
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
{n.message.description}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
case "info":
|
||||||
)}
|
return <div class="rounded-md bg-green-50 border-4 border-green-600 p-6">
|
||||||
{!error ? undefined : (
|
<div class="flex">
|
||||||
<ErrorBanner
|
<div class="flex-shrink-0">
|
||||||
error={error}
|
<svg class="h-8 w-8 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
onClear={() => {
|
<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" />
|
||||||
setError(undefined);
|
</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>
|
||||||
</div>
|
|
||||||
);
|
<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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
<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>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TestingTag(): VNode {
|
function TestingTag(): VNode {
|
||||||
@ -392,12 +428,12 @@ function TestingTag(): VNode {
|
|||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer class="absolute bottom-4">
|
<footer class="bottom-4 mb-4">
|
||||||
<div class="mt-8 mx-8 md:order-1 md:mt-0">
|
<div class="mt-8 mx-8 md:order-1 md:mt-0">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs leading-5 text-gray-400">
|
<p class="text-xs leading-5 text-gray-400">
|
||||||
You can learn more about GNU Taler on our{" "}
|
You can learn more about GNU Taler on our{" "}
|
||||||
<a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">main website</a>.
|
<a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">main website</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow:1" />
|
<div style="flex-grow:1" />
|
||||||
@ -418,4 +454,9 @@ function WelcomeAccount(): VNode {
|
|||||||
Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
|
Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountBalance(): VNode {
|
||||||
|
|
||||||
|
return <div>KUDOS 100.00</div>
|
||||||
|
}
|
||||||
|
@ -17,17 +17,21 @@ import {
|
|||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
TranslatedString,
|
TranslatedString
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
|
notifyInfo,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Cashouts } from "../components/Cashouts/index.js";
|
import { Cashouts } from "../components/Cashouts/index.js";
|
||||||
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { useAccountDetails } from "../hooks/access.js";
|
import { useAccountDetails } from "../hooks/access.js";
|
||||||
import {
|
import {
|
||||||
@ -42,12 +46,9 @@ import {
|
|||||||
undefinedIfEmpty,
|
undefinedIfEmpty,
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
||||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
|
||||||
import { LoginForm } from "./LoginForm.js";
|
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
|
||||||
import { handleNotOkResult } from "./HomePage.js";
|
import { handleNotOkResult } from "./HomePage.js";
|
||||||
import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { Amount } from "./WalletWithdrawForm.js";
|
import { Amount } from "./PaytoWireTransferForm.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -225,7 +226,6 @@ function CreateCashout({
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const ratiosResult = useRatiosAndFeeConfig();
|
const ratiosResult = useRatiosAndFeeConfig();
|
||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
const {
|
const {
|
||||||
estimateByCredit: calculateFromCredit,
|
estimateByCredit: calculateFromCredit,
|
||||||
estimateByDebit: calculateFromDebit,
|
estimateByDebit: calculateFromDebit,
|
||||||
@ -268,15 +268,15 @@ function CreateCashout({
|
|||||||
calculateFromDebit(amount, sellFee, sellRate)
|
calculateFromDebit(amount, sellFee, sellRate)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
setCalc(r);
|
setCalc(r);
|
||||||
saveError(undefined);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
saveError(
|
notify(
|
||||||
error instanceof RequestError
|
error instanceof RequestError
|
||||||
? buildRequestErrorMessage(i18n, error.cause)
|
? buildRequestErrorMessage(i18n, error.cause)
|
||||||
: {
|
: {
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Could not estimate the cashout`,
|
title: i18n.str`Could not estimate the cashout`,
|
||||||
description: error.message,
|
description: error.message as TranslatedString
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -284,13 +284,13 @@ function CreateCashout({
|
|||||||
calculateFromCredit(amount, sellFee, sellRate)
|
calculateFromCredit(amount, sellFee, sellRate)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
setCalc(r);
|
setCalc(r);
|
||||||
saveError(undefined);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
saveError(
|
notify(
|
||||||
error instanceof RequestError
|
error instanceof RequestError
|
||||||
? buildRequestErrorMessage(i18n, error.cause)
|
? buildRequestErrorMessage(i18n, error.cause)
|
||||||
: {
|
: {
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Could not estimate the cashout`,
|
title: i18n.str`Could not estimate the cashout`,
|
||||||
description: error.message,
|
description: error.message,
|
||||||
},
|
},
|
||||||
@ -321,9 +321,6 @@ function CreateCashout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
<h1>New cashout</h1>
|
<h1>New cashout</h1>
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@ -341,13 +338,15 @@ function CreateCashout({
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label for="amount">
|
||||||
{form.isDebit
|
{form.isDebit
|
||||||
? i18n.str`Amount to send`
|
? i18n.str`Amount to send`
|
||||||
: i18n.str`Amount to receive`}
|
: i18n.str`Amount to receive`}
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="amount"
|
||||||
currency={amount.currency}
|
currency={amount.currency}
|
||||||
value={form.amount}
|
value={form.amount}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
@ -376,24 +375,27 @@ function CreateCashout({
|
|||||||
<input value={sellRate} disabled />
|
<input value={sellRate} disabled />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Balance now`}</label>
|
<label for="balance-now">{i18n.str`Balance now`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="banace-now"
|
||||||
currency={balance.currency}
|
currency={balance.currency}
|
||||||
value={Amounts.stringifyValue(balance)}
|
value={Amounts.stringifyValue(balance)}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label
|
<label for="total-cost"
|
||||||
style={{ fontWeight: "bold", color: "red" }}
|
style={{ fontWeight: "bold", color: "red" }}
|
||||||
>{i18n.str`Total cost`}</label>
|
>{i18n.str`Total cost`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="total-cost"
|
||||||
currency={balance.currency}
|
currency={balance.currency}
|
||||||
value={Amounts.stringifyValue(calc.debit)}
|
value={Amounts.stringifyValue(calc.debit)}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Balance after`}</label>
|
<label for="balance-after">{i18n.str`Balance after`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="balance-after"
|
||||||
currency={balance.currency}
|
currency={balance.currency}
|
||||||
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
|
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
|
||||||
/>
|
/>
|
||||||
@ -401,16 +403,18 @@ function CreateCashout({
|
|||||||
{Amounts.isZero(sellFee) ? undefined : (
|
{Amounts.isZero(sellFee) ? undefined : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Amount after conversion`}</label>
|
<label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="amount-conversion"
|
||||||
currency={fiatCurrency}
|
currency={fiatCurrency}
|
||||||
value={Amounts.stringifyValue(calc.beforeFee)}
|
value={Amounts.stringifyValue(calc.beforeFee)}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Cashout fee`}</label>
|
<label form="cashout-fee">{i18n.str`Cashout fee`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="cashout-fee"
|
||||||
currency={fiatCurrency}
|
currency={fiatCurrency}
|
||||||
value={Amounts.stringifyValue(sellFee)}
|
value={Amounts.stringifyValue(sellFee)}
|
||||||
/>
|
/>
|
||||||
@ -418,10 +422,11 @@ function CreateCashout({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label
|
<label for="total"
|
||||||
style={{ fontWeight: "bold", color: "green" }}
|
style={{ fontWeight: "bold", color: "green" }}
|
||||||
>{i18n.str`Total cashout transfer`}</label>
|
>{i18n.str`Total cashout transfer`}</label>
|
||||||
<Amount
|
<Amount
|
||||||
|
name="total"
|
||||||
currency={fiatCurrency}
|
currency={fiatCurrency}
|
||||||
value={Amounts.stringifyValue(calc.credit)}
|
value={Amounts.stringifyValue(calc.credit)}
|
||||||
/>
|
/>
|
||||||
@ -511,7 +516,7 @@ function CreateCashout({
|
|||||||
onComplete(res.data.uuid);
|
onComplete(res.data.uuid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.BadRequest
|
status === HttpStatusCode.BadRequest
|
||||||
@ -530,14 +535,13 @@ function CreateCashout({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(
|
||||||
title: i18n.str`Operation failed, please report`,
|
i18n.str`Operation failed, please report`,
|
||||||
description:
|
(error instanceof Error
|
||||||
error instanceof Error
|
? error.message
|
||||||
? error.message
|
: JSON.stringify(error)) as TranslatedString
|
||||||
: JSON.stringify(error),
|
)
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -565,7 +569,6 @@ export function ShowCashoutDetails({
|
|||||||
const result = useCashoutDetails(id);
|
const result = useCashoutDetails(id);
|
||||||
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
|
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
|
||||||
const [code, setCode] = useState<string | undefined>(undefined);
|
const [code, setCode] = useState<string | undefined>(undefined);
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
if (!result.ok) return onLoadNotOk(result);
|
if (!result.ok) return onLoadNotOk(result);
|
||||||
const errors = undefinedIfEmpty({
|
const errors = undefinedIfEmpty({
|
||||||
code: !code ? i18n.str`required` : undefined,
|
code: !code ? i18n.str`required` : undefined,
|
||||||
@ -574,9 +577,6 @@ export function ShowCashoutDetails({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Cashout details {id}</h1>
|
<h1>Cashout details {id}</h1>
|
||||||
{error && (
|
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
||||||
)}
|
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
@ -661,7 +661,7 @@ export function ShowCashoutDetails({
|
|||||||
onCancel();
|
onCancel();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.NotFound
|
status === HttpStatusCode.NotFound
|
||||||
@ -672,14 +672,13 @@ export function ShowCashoutDetails({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(
|
||||||
title: i18n.str`Operation failed, please report`,
|
i18n.str`Operation failed, please report`,
|
||||||
description:
|
(error instanceof Error
|
||||||
error instanceof Error
|
? error.message
|
||||||
? error.message
|
: JSON.stringify(error)) as TranslatedString
|
||||||
: JSON.stringify(error),
|
)
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -699,7 +698,7 @@ export function ShowCashoutDetails({
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
if (error instanceof RequestError) {
|
||||||
saveError(
|
notify(
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
onClientError: (status) =>
|
onClientError: (status) =>
|
||||||
status === HttpStatusCode.NotFound
|
status === HttpStatusCode.NotFound
|
||||||
@ -714,14 +713,13 @@ export function ShowCashoutDetails({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
saveError({
|
notifyError(
|
||||||
title: i18n.str`Operation failed, please report`,
|
i18n.str`Operation failed, please report`,
|
||||||
description:
|
(error instanceof Error
|
||||||
error instanceof Error
|
? error.message
|
||||||
? error.message
|
: JSON.stringify(error)) as TranslatedString
|
||||||
: JSON.stringify(error),
|
)
|
||||||
});
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
|
TranslatedString,
|
||||||
parseWithdrawUri,
|
parseWithdrawUri,
|
||||||
stringifyWithdrawUri,
|
stringifyWithdrawUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
@ -24,18 +25,20 @@ import {
|
|||||||
ErrorType,
|
ErrorType,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
||||||
import { notifyError, notifyInfo } from "../hooks/notification.js";
|
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { AccountPage } from "./AccountPage/index.js";
|
import { AccountPage } from "./AccountPage/index.js";
|
||||||
import { AdminPage } from "./AdminPage.js";
|
import { AdminPage } from "./AdminPage.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
||||||
|
import { error } from "console";
|
||||||
|
|
||||||
const logger = new Logger("AccountPage");
|
const logger = new Logger("AccountPage");
|
||||||
|
|
||||||
@ -100,9 +103,10 @@ export function WithdrawalOperationPage({
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
if (!parsedUri) {
|
if (!parsedUri) {
|
||||||
notifyError({
|
notifyError(
|
||||||
title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
|
i18n.str`The Withdrawal URI is not valid: "${uri}"`,
|
||||||
});
|
undefined
|
||||||
|
);
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,46 +136,46 @@ export function handleNotOkResult(
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
case ErrorType.TIMEOUT: {
|
case ErrorType.TIMEOUT: {
|
||||||
notifyError({
|
notifyError(i18n.str`Request timeout, try again later.`, undefined);
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.CLIENT: {
|
case ErrorType.CLIENT: {
|
||||||
if (result.status === HttpStatusCode.Unauthorized) {
|
if (result.status === HttpStatusCode.Unauthorized) {
|
||||||
notifyError({
|
notifyError(i18n.str`Wrong credentials`, undefined);
|
||||||
title: i18n.str`Wrong credentials`,
|
|
||||||
});
|
|
||||||
return <LoginForm onRegister={onRegister} />;
|
return <LoginForm onRegister={onRegister} />;
|
||||||
}
|
}
|
||||||
const errorData = result.payload;
|
const errorData = result.payload;
|
||||||
notifyError({
|
notify({
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Could not load due to a client error`,
|
title: i18n.str`Could not load due to a client error`,
|
||||||
description: errorData.error.description,
|
description: errorData.error.description as TranslatedString,
|
||||||
debug: JSON.stringify(result),
|
debug: JSON.stringify(result),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.SERVER: {
|
case ErrorType.SERVER: {
|
||||||
notifyError({
|
notify({
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Server returned with error`,
|
title: i18n.str`Server returned with error`,
|
||||||
description: result.payload.error.description,
|
description: result.payload.error.description as TranslatedString,
|
||||||
debug: JSON.stringify(result.payload),
|
debug: JSON.stringify(result.payload),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.UNREADABLE: {
|
case ErrorType.UNREADABLE: {
|
||||||
notifyError({
|
notify({
|
||||||
|
type:"error",
|
||||||
title: i18n.str`Unexpected error.`,
|
title: i18n.str`Unexpected error.`,
|
||||||
description: `Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
|
description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
|
||||||
debug: JSON.stringify(result),
|
debug: JSON.stringify(result),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.UNEXPECTED: {
|
case ErrorType.UNEXPECTED: {
|
||||||
notifyError({
|
notify({
|
||||||
|
type:"error",
|
||||||
title: i18n.str`Unexpected error.`,
|
title: i18n.str`Unexpected error.`,
|
||||||
description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
|
description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
|
||||||
debug: JSON.stringify(result),
|
debug: JSON.stringify(result),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -14,16 +14,14 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { useCredentialsChecker } from "../hooks/backend.js";
|
import { useCredentialsChecker } from "../hooks/backend.js";
|
||||||
import { ErrorMessage } from "../hooks/notification.js";
|
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { undefinedIfEmpty } from "../utils.js";
|
||||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
|
||||||
import { USERNAME_REGEX } from "./RegistrationPage.js";
|
import { USERNAME_REGEX } from "./RegistrationPage.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
|
|
||||||
@ -36,177 +34,202 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
const [password, setPassword] = useState<string | undefined>();
|
const [password, setPassword] = useState<string | undefined>();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const testLogin = useCredentialsChecker();
|
const testLogin = useCredentialsChecker();
|
||||||
const [error, saveError] = useState<ErrorMessage | undefined>();
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
useEffect(function focusInput() {
|
useEffect(function focusInput() {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
const [busy, setBusy] = useState<Record<string, undefined>>()
|
||||||
|
|
||||||
const errors = undefinedIfEmpty({
|
const errors = undefinedIfEmpty({
|
||||||
username: !username
|
username: !username
|
||||||
? i18n.str`Missing username`
|
? i18n.str`Missing username`
|
||||||
: !USERNAME_REGEX.test(username)
|
: !USERNAME_REGEX.test(username)
|
||||||
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
||||||
: undefined,
|
: undefined,
|
||||||
password: !password ? i18n.str`Missing password` : undefined,
|
password: !password ? i18n.str`Missing password` : undefined,
|
||||||
});
|
}) ?? busy;
|
||||||
|
|
||||||
|
function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
|
||||||
|
notifyError(title, description, debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLogin() {
|
||||||
|
if (!username || !password) return;
|
||||||
|
setBusy({})
|
||||||
|
const testResult = await testLogin(username, password);
|
||||||
|
if (testResult.valid) {
|
||||||
|
backend.logIn({ username, password });
|
||||||
|
} else {
|
||||||
|
if (testResult.requestError) {
|
||||||
|
const { cause } = testResult;
|
||||||
|
switch (cause.type) {
|
||||||
|
case ErrorType.CLIENT: {
|
||||||
|
if (cause.status === HttpStatusCode.Unauthorized) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Wrong credentials for "${username}"`,
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
if (cause.status === HttpStatusCode.NotFound) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Account not found`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Could not load due to a client error`,
|
||||||
|
description: cause.payload.error.description,
|
||||||
|
debug: JSON.stringify(cause.payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.SERVER: {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Server had a problem, try again later or report.`,
|
||||||
|
description: cause.payload.error.description,
|
||||||
|
debug: JSON.stringify(cause.payload),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.TIMEOUT: {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Request timeout, try again later.`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ErrorType.UNREADABLE: {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Unexpected error.`,
|
||||||
|
description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
|
||||||
|
debug: JSON.stringify(cause),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Unexpected error, please report.`,
|
||||||
|
description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
|
||||||
|
debug: JSON.stringify(cause),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Unexpected error, please report.`,
|
||||||
|
debug: JSON.stringify(testResult.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
backend.logOut();
|
||||||
|
}
|
||||||
|
setPassword(undefined);
|
||||||
|
setBusy(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
|
<h1 class="nav"></h1>
|
||||||
{error && (
|
{/* {error && (
|
||||||
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
||||||
)}
|
)} */}
|
||||||
<div class="login-div">
|
|
||||||
<form
|
|
||||||
class="login-form"
|
|
||||||
noValidate
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="off"
|
|
||||||
>
|
|
||||||
<div class="pure-form">
|
|
||||||
<h2>{i18n.str`Please login!`}</h2>
|
|
||||||
<p class="unameFieldLabel loginFieldLabel formFieldLabel">
|
|
||||||
<label for="username">{i18n.str`Username:`}</label>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
autoFocus
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
id="username"
|
|
||||||
value={username ?? ""}
|
|
||||||
enterkeyhint="next"
|
|
||||||
placeholder="Username"
|
|
||||||
autocomplete="username"
|
|
||||||
required
|
|
||||||
onInput={(e): void => {
|
|
||||||
setUsername(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.username}
|
|
||||||
isDirty={username !== undefined}
|
|
||||||
/>
|
|
||||||
<p class="passFieldLabel loginFieldLabel formFieldLabel">
|
|
||||||
<label for="password">{i18n.str`Password:`}</label>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
enterkeyhint="send"
|
|
||||||
value={password ?? ""}
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
onInput={(e): void => {
|
|
||||||
setPassword(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.password}
|
|
||||||
isDirty={password !== undefined}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="pure-button pure-button-primary"
|
|
||||||
disabled={!!errors}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!username || !password) return;
|
|
||||||
const testResult = await testLogin(username, password);
|
|
||||||
if (testResult.valid) {
|
|
||||||
backend.logIn({ username, password });
|
|
||||||
} else {
|
|
||||||
if (testResult.requestError) {
|
|
||||||
const { cause } = testResult;
|
|
||||||
switch (cause.type) {
|
|
||||||
case ErrorType.CLIENT: {
|
|
||||||
if (cause.status === HttpStatusCode.Unauthorized) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Wrong credentials for "${username}"`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (cause.status === HttpStatusCode.NotFound) {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Account not found`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Could not load due to a client error`,
|
|
||||||
description: cause.payload.error.description,
|
|
||||||
debug: JSON.stringify(cause.payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.SERVER: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Server had a problem, try again later or report.`,
|
|
||||||
description: cause.payload.error.description,
|
|
||||||
debug: JSON.stringify(cause.payload),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.TIMEOUT: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Request timeout, try again later.`,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ErrorType.UNREADABLE: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error.`,
|
|
||||||
description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}`,
|
|
||||||
debug: JSON.stringify(cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error, please report.`,
|
|
||||||
description: `Diagnostic from ${cause.info?.url} is "${cause.message}"`,
|
|
||||||
debug: JSON.stringify(cause),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
saveError({
|
|
||||||
title: i18n.str`Unexpected error, please report.`,
|
|
||||||
debug: JSON.stringify(testResult.error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
backend.logOut();
|
|
||||||
}
|
|
||||||
setUsername(undefined);
|
|
||||||
setPassword(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n.str`Login`}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{bankUiSettings.allowRegistrations && onRegister ? (
|
<div class="flex min-h-full flex-col justify-center">
|
||||||
<button
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
class="pure-button pure-button-secondary btn-cancel"
|
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" noValidate
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium leading-6 text-gray-900">
|
||||||
|
<i18n.Translate>Username</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
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"
|
||||||
|
value={username ?? ""}
|
||||||
|
enterkeyhint="next"
|
||||||
|
placeholder="identification"
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
onInput={(e): void => {
|
||||||
|
setUsername(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.username}
|
||||||
|
isDirty={username !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
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"
|
||||||
|
enterkeyhint="send"
|
||||||
|
value={password ?? ""}
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
onInput={(e): void => {
|
||||||
|
setPassword(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.password}
|
||||||
|
isDirty={password !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={!!errors}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
onRegister();
|
doLogin()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.str`Register`}
|
<i18n.Translate>Log in</i18n.Translate>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
</div>
|
||||||
<div />
|
</form>
|
||||||
)}
|
|
||||||
</div>
|
{bankUiSettings.allowRegistrations && onRegister &&
|
||||||
</form>
|
<p class="mt-10 text-center text-sm text-gray-500 border-t">
|
||||||
|
<button type="submit"
|
||||||
|
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onRegister()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Register</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AmountJson } from "@gnu-taler/taler-util";
|
import { AmountJson } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { notifyInfo } from "../hooks/notification.js";
|
|
||||||
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
|
||||||
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
|
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
@ -31,7 +30,8 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings, updateSettings] = useSettings();
|
||||||
|
|
||||||
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
|
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
|
||||||
|
// const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
|
||||||
|
|
||||||
return (<fieldset>
|
return (<fieldset>
|
||||||
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
|
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
|
||||||
@ -41,34 +41,32 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
|
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
|
||||||
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
|
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
|
||||||
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "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" + (tab === "charge-wallet" ? "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" onClick={() => {
|
||||||
setTab("charge-wallet")
|
setTab("charge-wallet")
|
||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-0-label" class="block text-sm font-semibold font-medium text-gray-900">
|
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
|
||||||
<i18n.Translate>a Taler Wallet</i18n.Translate>
|
<i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{/* <!-- Not Checked: "invisible" --> */}
|
|
||||||
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
<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>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
|
|
||||||
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "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" + (tab === "wire-transfer" ? "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" onClick={() => {
|
||||||
setTab("wire-transfer")
|
setTab("wire-transfer")
|
||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-1-label" class="block text-sm font-semibold font-medium text-gray-900">
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
<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">
|
||||||
@ -88,6 +86,9 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
onSuccess={(id) => {
|
onSuccess={(id) => {
|
||||||
updateSettings("currentWithdrawalOperationId", id);
|
updateSettings("currentWithdrawalOperationId", id);
|
||||||
}}
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setTab(undefined)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "wire-transfer" && (
|
{tab === "wire-transfer" && (
|
||||||
@ -97,32 +98,11 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
notifyInfo(i18n.str`Wire transfer created!`);
|
notifyInfo(i18n.str`Wire transfer created!`);
|
||||||
}}
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setTab(undefined)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</fieldset>)
|
</fieldset>)
|
||||||
{/* return (
|
|
||||||
<article>
|
|
||||||
<div class="payments">
|
|
||||||
<div class="tab">
|
|
||||||
<button
|
|
||||||
class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
|
|
||||||
onClick={(): void => {
|
|
||||||
setTab("charge-wallet");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n.str`Withdraw `}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
|
|
||||||
onClick={(): void => {
|
|
||||||
setTab("wire-transfer");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n.str`Wire transfer`}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
); */}
|
|
||||||
}
|
}
|
||||||
|
@ -19,17 +19,21 @@ import {
|
|||||||
Amounts,
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
parsePaytoUri
|
TranslatedString,
|
||||||
|
buildPayto,
|
||||||
|
parsePaytoUri,
|
||||||
|
stringifyPaytoUri
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode, Fragment } from "preact";
|
import { h, VNode, Fragment, Ref } from "preact";
|
||||||
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 { useAccessAPI } from "../hooks/access.js";
|
import { useAccessAPI } from "../hooks/access.js";
|
||||||
import { notifyError } from "../hooks/notification.js";
|
|
||||||
import {
|
import {
|
||||||
buildRequestErrorMessage,
|
buildRequestErrorMessage,
|
||||||
undefinedIfEmpty,
|
undefinedIfEmpty,
|
||||||
@ -41,10 +45,12 @@ const logger = new Logger("PaytoWireTransferForm");
|
|||||||
export function PaytoWireTransferForm({
|
export function PaytoWireTransferForm({
|
||||||
focus,
|
focus,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
limit,
|
limit,
|
||||||
}: {
|
}: {
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
|
onCancel: (() => void) | undefined;
|
||||||
limit: AmountJson;
|
limit: AmountJson;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const [isRawPayto, setIsRawPayto] = useState(false);
|
const [isRawPayto, setIsRawPayto] = useState(false);
|
||||||
@ -105,7 +111,51 @@ export function PaytoWireTransferForm({
|
|||||||
? 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),
|
||||||
});
|
});
|
||||||
// if (!isRawPayto) {
|
|
||||||
|
async function doSend() {
|
||||||
|
let paytoUri: string | undefined;
|
||||||
|
|
||||||
|
if (rawPaytoInput) {
|
||||||
|
paytoUri = rawPaytoInput
|
||||||
|
} else {
|
||||||
|
if (!iban || !subject) return;
|
||||||
|
const ibanPayto = buildPayto("iban", iban, undefined);
|
||||||
|
ibanPayto.params.message = encodeURIComponent(subject);
|
||||||
|
paytoUri = stringifyPaytoUri(ibanPayto);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createTransaction({
|
||||||
|
paytoUri,
|
||||||
|
amount: `${limit.currency}:${amount}`,
|
||||||
|
});
|
||||||
|
onSuccess();
|
||||||
|
setAmount(undefined);
|
||||||
|
setIban(undefined);
|
||||||
|
setSubject(undefined);
|
||||||
|
rawPaytoInputSetter(undefined)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.BadRequest
|
||||||
|
? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
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">
|
||||||
<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>Transfer details</i18n.Translate></h2>
|
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Transfer details</i18n.Translate></h2>
|
||||||
@ -118,7 +168,7 @@ export function PaytoWireTransferForm({
|
|||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
|
<span class="block text-sm font-medium text-gray-900">
|
||||||
<i18n.Translate>form</i18n.Translate>
|
<i18n.Translate>form</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -133,7 +183,7 @@ export function PaytoWireTransferForm({
|
|||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
<span class="block text-sm font-medium text-gray-900">
|
||||||
<i18n.Translate>payto://</i18n.Translate>
|
<i18n.Translate>payto://</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -143,23 +193,31 @@ export function PaytoWireTransferForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
|
<form
|
||||||
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="px-4 py-6 sm:p-8">
|
<div class="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">
|
||||||
{!isRawPayto ?
|
{!isRawPayto ?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-5">
|
||||||
<label for="first-name" 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`Account number`}</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
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"
|
||||||
id="iban"
|
|
||||||
name="iban"
|
name="iban"
|
||||||
|
id="iban"
|
||||||
value={iban ?? ""}
|
value={iban ?? ""}
|
||||||
placeholder="CC0123456789"
|
placeholder="CC0123456789"
|
||||||
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
pattern={ibanRegex}
|
pattern={ibanRegex}
|
||||||
onInput={(e): void => {
|
onInput={(e): void => {
|
||||||
@ -171,21 +229,18 @@ export function PaytoWireTransferForm({
|
|||||||
isDirty={iban !== undefined}
|
isDirty={iban !== undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-gray-500" id="email-description">the receiver of the money</p>
|
<p class="mt-2 text-sm text-gray-500" >the receiver of the money</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-5">
|
||||||
</div>
|
<label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label>
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
|
||||||
<label for="first-name" 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"
|
||||||
placeholder="subject"
|
placeholder="subject"
|
||||||
value={subject ?? ""}
|
value={subject ?? ""}
|
||||||
required
|
required
|
||||||
@ -198,37 +253,40 @@ export function PaytoWireTransferForm({
|
|||||||
isDirty={subject !== undefined}
|
isDirty={subject !== undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-gray-500" id="email-description">some text to identify the transfer</p>
|
<p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
<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
|
||||||
|
message={errorsWire?.subject}
|
||||||
|
isDirty={subject !== undefined}
|
||||||
|
/>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
|
||||||
<label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
|
|
||||||
<div class="mt-2">
|
|
||||||
<input type="text" name="first-name" id="first-name" autocomplete="given-name" 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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sm:col-span-3">
|
|
||||||
</div>
|
|
||||||
</Fragment> :
|
</Fragment> :
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div class="sm:col-span-6">
|
<div class="sm:col-span-6">
|
||||||
<label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label>
|
<label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
name="address"
|
name="address"
|
||||||
|
id="address"
|
||||||
type="text"
|
type="text"
|
||||||
size={50}
|
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}
|
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}
|
||||||
id="address"
|
|
||||||
value={rawPaytoInput ?? ""}
|
value={rawPaytoInput ?? ""}
|
||||||
required
|
required
|
||||||
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
|
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
|
||||||
// pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
|
|
||||||
onInput={(e): void => {
|
onInput={(e): void => {
|
||||||
rawPaytoInputSetter(e.currentTarget.value);
|
rawPaytoInputSetter(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@ -244,9 +302,23 @@ export function PaytoWireTransferForm({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end 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">
|
||||||
<button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button>
|
{onCancel ?
|
||||||
<button type="submit" class="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">
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
: <div />
|
||||||
|
}
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doSend()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<i18n.Translate>Send</i18n.Translate>
|
<i18n.Translate>Send</i18n.Translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -262,8 +334,6 @@ export function PaytoWireTransferForm({
|
|||||||
// onSubmit={(e) => {
|
// onSubmit={(e) => {
|
||||||
// e.preventDefault();
|
// e.preventDefault();
|
||||||
// }}
|
// }}
|
||||||
// autoCapitalize="none"
|
|
||||||
// autoCorrect="off"
|
|
||||||
// >
|
// >
|
||||||
// <label for="iban">{i18n.str`Receiver IBAN:`}</label>
|
// <label for="iban">{i18n.str`Receiver IBAN:`}</label>
|
||||||
|
|
||||||
@ -318,39 +388,7 @@ export function PaytoWireTransferForm({
|
|||||||
// if (!(iban && subject && amount)) {
|
// if (!(iban && subject && amount)) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
// const ibanPayto = buildPayto("iban", iban, undefined);
|
|
||||||
// ibanPayto.params.message = encodeURIComponent(subject);
|
|
||||||
// const paytoUri = stringifyPaytoUri(ibanPayto);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// await createTransaction({
|
|
||||||
// paytoUri,
|
|
||||||
// amount: `${limit.currency}:${amount}`,
|
|
||||||
// });
|
|
||||||
// onSuccess();
|
|
||||||
// setAmount(undefined);
|
|
||||||
// setIban(undefined);
|
|
||||||
// setSubject(undefined);
|
|
||||||
// } catch (error) {
|
|
||||||
// if (error instanceof RequestError) {
|
|
||||||
// notifyError(
|
|
||||||
// buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
// onClientError: (status) =>
|
|
||||||
// status === HttpStatusCode.BadRequest
|
|
||||||
// ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
|
|
||||||
// : undefined,
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// notifyError({
|
|
||||||
// title: i18n.str`Operation failed, please report`,
|
|
||||||
// description:
|
|
||||||
// error instanceof Error
|
|
||||||
// ? error.message
|
|
||||||
// : JSON.stringify(error),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }}
|
// }}
|
||||||
// />
|
// />
|
||||||
// <input
|
// <input
|
||||||
@ -389,3 +427,46 @@ export function PaytoWireTransferForm({
|
|||||||
// </div>
|
// </div>
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
export function Amount(
|
||||||
|
{
|
||||||
|
currency,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
error?: string;
|
||||||
|
currency: string;
|
||||||
|
name: string;
|
||||||
|
value: string | undefined;
|
||||||
|
onChange?: (s: string) => void;
|
||||||
|
},
|
||||||
|
ref: Ref<HTMLInputElement>,
|
||||||
|
): 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">
|
||||||
|
<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"
|
||||||
|
placeholder="0.00" aria-describedby="price-currency"
|
||||||
|
ref={ref}
|
||||||
|
name={name}
|
||||||
|
id={name}
|
||||||
|
autocomplete="off"
|
||||||
|
value={value ?? ""}
|
||||||
|
disabled={!onChange}
|
||||||
|
onInput={(e): void => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ShowInputErrorLabel message={error} isDirty={value !== undefined} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -17,17 +17,19 @@
|
|||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
stringifyWithdrawUri,
|
stringifyWithdrawUri,
|
||||||
|
TranslatedString,
|
||||||
WithdrawUriResult,
|
WithdrawUriResult,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
RequestError,
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import { QR } from "../components/QR.js";
|
import { QR } from "../components/QR.js";
|
||||||
import { useAccessAnonAPI } from "../hooks/access.js";
|
import { useAccessAnonAPI } from "../hooks/access.js";
|
||||||
import { notifyError } from "../hooks/notification.js";
|
|
||||||
import { buildRequestErrorMessage } from "../utils.js";
|
import { buildRequestErrorMessage } from "../utils.js";
|
||||||
|
|
||||||
export function QrCodeSection({
|
export function QrCodeSection({
|
||||||
@ -49,47 +51,87 @@ export function QrCodeSection({
|
|||||||
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
|
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
|
||||||
|
|
||||||
const { abortWithdrawal } = useAccessAnonAPI();
|
const { abortWithdrawal } = useAccessAnonAPI();
|
||||||
|
|
||||||
|
async function doAbort() {
|
||||||
|
try {
|
||||||
|
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
||||||
|
onAborted();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="main" class="content">
|
<Fragment>
|
||||||
<h1 class="nav">{i18n.str`Charge your GNU Taler wallet`}</h1>
|
<div class="bg-white shadow-xl sm:rounded-lg">
|
||||||
<article>
|
<div class="px-4 py-5 sm:p-6">
|
||||||
<div class="qr-div ">
|
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
<a href={talerWithdrawUri} class="pure-button pure-button-primary">
|
<i18n.Translate>If you have a Taler wallet installed in this device</i18n.Translate>
|
||||||
<i18n.Translate>Continue with GNU Taler</i18n.Translate>
|
</h3>
|
||||||
</a>
|
<div class="mt-4">
|
||||||
<p>{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}</p>
|
<a href={talerWithdrawUri}
|
||||||
<QR text={talerWithdrawUri} />
|
// class="text-sm font-semibold leading-6 text-gray-900 btn "
|
||||||
<a
|
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="pure-button btn-cancel"
|
>
|
||||||
onClick={async (e) => {
|
<i18n.Translate>Click here to start</i18n.Translate>
|
||||||
e.preventDefault();
|
</a>
|
||||||
try {
|
</div>
|
||||||
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
<div class="mt-4 max-w-xl text-sm text-gray-500">
|
||||||
onAborted();
|
<p><i18n.Translate>
|
||||||
} catch (error) {
|
You will see the details of the operation in your wallet including the fees (if applies).
|
||||||
if (error instanceof RequestError) {
|
If you still one you can install it from <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">here</a>.
|
||||||
notifyError(
|
</i18n.Translate></p>
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
</div>
|
||||||
onClientError: (status) =>
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 ">
|
||||||
status === HttpStatusCode.Conflict
|
<div />
|
||||||
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
<button type="button"
|
||||||
: undefined,
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||||
}),
|
onClick={doAbort}
|
||||||
);
|
>
|
||||||
} else {
|
Cancel withdrawal
|
||||||
notifyError({
|
</button>
|
||||||
title: i18n.str`Operation failed, please report`,
|
</div>
|
||||||
description:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>{i18n.str`Cancel`}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</section>
|
|
||||||
|
<div class="bg-white shadow-xl sm:rounded-lg mt-8">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
<i18n.Translate>Or if you have the wallet in another device</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-4 max-w-xl text-sm text-gray-500">
|
||||||
|
<i18n.Translate>Scan the QR below to start the withdrawal</i18n.Translate>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 max-w-md ml-auto mr-auto">
|
||||||
|
<QR text={talerWithdrawUri} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
|
<button type="button"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||||
|
onClick={doAbort}
|
||||||
|
>
|
||||||
|
Cancel withdrawal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,19 +13,21 @@
|
|||||||
You should have received a copy of the GNU General Public License along with
|
You should have received a copy of the GNU General Public License along with
|
||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { useTestingAPI } from "../hooks/access.js";
|
import { useTestingAPI } from "../hooks/access.js";
|
||||||
import { notifyError } from "../hooks/notification.js";
|
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
|
import { getRandomPassword, getRandomUsername } from "./rnd.js";
|
||||||
|
|
||||||
const logger = new Logger("RegistrationPage");
|
const logger = new Logger("RegistrationPage");
|
||||||
|
|
||||||
@ -61,147 +63,214 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
|
|||||||
username: !username
|
username: !username
|
||||||
? i18n.str`Missing username`
|
? i18n.str`Missing username`
|
||||||
: !USERNAME_REGEX.test(username)
|
: !USERNAME_REGEX.test(username)
|
||||||
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
||||||
: undefined,
|
: undefined,
|
||||||
password: !password ? i18n.str`Missing password` : undefined,
|
password: !password ? i18n.str`Missing password` : undefined,
|
||||||
repeatPassword: !repeatPassword
|
repeatPassword: !repeatPassword
|
||||||
? i18n.str`Missing password`
|
? i18n.str`Missing password`
|
||||||
: repeatPassword !== password
|
: repeatPassword !== password
|
||||||
? i18n.str`Passwords don't match`
|
? i18n.str`Passwords don't match`
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function doRegistrationStep() {
|
||||||
|
if (!username || !password) return;
|
||||||
|
try {
|
||||||
|
await register({ username, password });
|
||||||
|
setUsername(undefined);
|
||||||
|
backend.logIn({ username, password });
|
||||||
|
onComplete();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`That username is already taken`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPassword(undefined);
|
||||||
|
setRepeatPassword(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delay(ms: number):Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(undefined);
|
||||||
|
}, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function doRandomRegistration(tries: number = 3) {
|
||||||
|
const user = getRandomUsername();
|
||||||
|
const pass = getRandomPassword();
|
||||||
|
try {
|
||||||
|
setUsername(undefined);
|
||||||
|
setPassword(undefined);
|
||||||
|
setRepeatPassword(undefined);
|
||||||
|
await register({ username: user, password: pass });
|
||||||
|
backend.logIn({ username: user, password: pass });
|
||||||
|
onComplete();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
if (tries > 0) {
|
||||||
|
await delay(200)
|
||||||
|
await doRandomRegistration(tries-1)
|
||||||
|
} else {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`Could not create a random user`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
|
<h1 class="nav"></h1>
|
||||||
<article>
|
|
||||||
<div class="register-div">
|
<div class="flex min-h-full flex-col justify-center">
|
||||||
<form
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
class="register-form"
|
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Sign up!`}</h2>
|
||||||
noValidate
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" noValidate
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
>
|
>
|
||||||
<div class="pure-form">
|
<div>
|
||||||
<h2>{i18n.str`Please register!`}</h2>
|
<label for="username" class="block text-sm font-medium leading-6 text-gray-900">
|
||||||
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
|
<i18n.Translate>Username</i18n.Translate>
|
||||||
<label for="register-un">{i18n.str`Username:`}</label>
|
</label>
|
||||||
</p>
|
<div class="mt-2">
|
||||||
<input
|
<input
|
||||||
id="register-un"
|
autoFocus
|
||||||
name="register-un"
|
type="text"
|
||||||
type="text"
|
name="username"
|
||||||
placeholder="Username"
|
id="username"
|
||||||
autocomplete="username"
|
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"
|
||||||
value={username ?? ""}
|
value={username ?? ""}
|
||||||
onInput={(e): void => {
|
enterkeyhint="next"
|
||||||
setUsername(e.currentTarget.value);
|
placeholder="identification"
|
||||||
}}
|
autocomplete="username"
|
||||||
/>
|
required
|
||||||
<ShowInputErrorLabel
|
onInput={(e): void => {
|
||||||
message={errors?.username}
|
setUsername(e.currentTarget.value);
|
||||||
isDirty={username !== undefined}
|
}}
|
||||||
/>
|
/>
|
||||||
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
|
<ShowInputErrorLabel
|
||||||
<label for="register-pw">{i18n.str`Password:`}</label>
|
message={errors?.username}
|
||||||
</p>
|
isDirty={username !== undefined}
|
||||||
<input
|
/>
|
||||||
type="password"
|
</div>
|
||||||
name="register-pw"
|
</div>
|
||||||
id="register-pw"
|
|
||||||
placeholder="Password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
value={password ?? ""}
|
|
||||||
required
|
|
||||||
onInput={(e): void => {
|
|
||||||
setPassword(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.password}
|
|
||||||
isDirty={password !== undefined}
|
|
||||||
/>
|
|
||||||
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
|
|
||||||
<label for="register-repeat">{i18n.str`Repeat Password:`}</label>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
name="register-repeat"
|
|
||||||
id="register-repeat"
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Same password"
|
|
||||||
value={repeatPassword ?? ""}
|
|
||||||
required
|
|
||||||
onInput={(e): void => {
|
|
||||||
setRepeatPassword(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.repeatPassword}
|
|
||||||
isDirty={repeatPassword !== undefined}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
class="pure-button pure-button-primary btn-register"
|
|
||||||
type="submit"
|
|
||||||
disabled={!!errors}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!username || !password) return;
|
<div>
|
||||||
try {
|
<div class="flex items-center justify-between">
|
||||||
const credentials = { username, password };
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||||
await register(credentials);
|
</div>
|
||||||
setUsername(undefined);
|
<div class="mt-2">
|
||||||
setPassword(undefined);
|
<input
|
||||||
setRepeatPassword(undefined);
|
type="password"
|
||||||
backend.logIn(credentials);
|
name="password"
|
||||||
onComplete();
|
id="password"
|
||||||
} catch (error) {
|
autocomplete="current-password"
|
||||||
if (error instanceof RequestError) {
|
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"
|
||||||
notifyError(
|
enterkeyhint="send"
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
value={password ?? ""}
|
||||||
onClientError: (status) =>
|
placeholder="Password"
|
||||||
status === HttpStatusCode.Conflict
|
required
|
||||||
? i18n.str`That username is already taken`
|
onInput={(e): void => {
|
||||||
: undefined,
|
setPassword(e.currentTarget.value);
|
||||||
}),
|
}}
|
||||||
);
|
/>
|
||||||
} else {
|
<ShowInputErrorLabel
|
||||||
notifyError({
|
message={errors?.password}
|
||||||
title: i18n.str`Operation failed, please report`,
|
isDirty={password !== undefined}
|
||||||
description:
|
/>
|
||||||
error instanceof Error
|
</div>
|
||||||
? error.message
|
</div>
|
||||||
: JSON.stringify(error),
|
|
||||||
});
|
<div>
|
||||||
}
|
<div class="flex items-center justify-between">
|
||||||
}
|
<label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900">Repeat assword</label>
|
||||||
}}
|
</div>
|
||||||
>
|
<div class="mt-2">
|
||||||
{i18n.str`Register`}
|
<input
|
||||||
</button>
|
type="password"
|
||||||
{/* FIXME: should use a different color */}
|
name="register-repeat"
|
||||||
<button
|
id="register-repeat"
|
||||||
class="pure-button pure-button-secondary btn-cancel"
|
autocomplete="current-password"
|
||||||
|
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"
|
||||||
|
enterkeyhint="send"
|
||||||
|
value={repeatPassword ?? ""}
|
||||||
|
placeholder="Same password"
|
||||||
|
required
|
||||||
|
onInput={(e): void => {
|
||||||
|
setRepeatPassword(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.repeatPassword}
|
||||||
|
isDirty={repeatPassword !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={!!errors}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setUsername(undefined);
|
doRegistrationStep()
|
||||||
setPassword(undefined);
|
|
||||||
setRepeatPassword(undefined);
|
|
||||||
onComplete();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.str`Cancel`}
|
<i18n.Translate>Confirm</i18n.Translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-10 text-center text-sm text-gray-500 border-t">
|
||||||
|
<button type="submit"
|
||||||
|
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doRandomRegistration()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Create a random user</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,19 +19,21 @@ import {
|
|||||||
Amounts,
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
|
TranslatedString,
|
||||||
parseWithdrawUri,
|
parseWithdrawUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Ref, VNode, h } from "preact";
|
import { VNode, h } from "preact";
|
||||||
|
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 { notifyError } from "../hooks/notification.js";
|
|
||||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { Amount } from "./PaytoWireTransferForm.js";
|
||||||
import { forwardRef } from "preact/compat";
|
|
||||||
|
|
||||||
const logger = new Logger("WalletWithdrawForm");
|
const logger = new Logger("WalletWithdrawForm");
|
||||||
const RefAmount = forwardRef(Amount);
|
const RefAmount = forwardRef(Amount);
|
||||||
@ -40,10 +42,12 @@ export function WalletWithdrawForm({
|
|||||||
focus,
|
focus,
|
||||||
limit,
|
limit,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
limit: AmountJson;
|
limit: AmountJson;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSuccess: (operationId: string) => void;
|
onSuccess: (operationId: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { createWithdrawal } = useAccessAPI();
|
const { createWithdrawal } = useAccessAPI();
|
||||||
@ -71,136 +75,195 @@ export function WalletWithdrawForm({
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
async function doStart() {
|
||||||
|
if (!parsedAmount) return;
|
||||||
|
try {
|
||||||
|
const result = await createWithdrawal({
|
||||||
|
amount: Amounts.stringify(parsedAmount),
|
||||||
|
});
|
||||||
|
const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
|
||||||
|
if (!uri) {
|
||||||
|
return notifyError(
|
||||||
|
i18n.str`Server responded with an invalid withdraw URI`,
|
||||||
|
i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
|
||||||
|
} else {
|
||||||
|
onSuccess(uri.withdrawalOperationId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The operation was rejected due to insufficient funds`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
|
<div class="px-4 sm:px-0">
|
||||||
|
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
<i18n.Translate>Upon starting you will receive the money in your digital wallet, if you don't have one please <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">install one from here</a></i18n.Translate>.
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
<i18n.Translate>After using your wallet you will be redirected here to confirm or cancel the operation.</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<form
|
<form
|
||||||
id="reserve-form"
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
class="pure-form"
|
|
||||||
name="tform"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p>
|
<div class="px-4 py-6 sm:p-8">
|
||||||
<label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
|
<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">
|
||||||
<RefAmount
|
<label for="withdraw-amount">{i18n.str`Amount`}</label>
|
||||||
currency={limit.currency}
|
<RefAmount
|
||||||
value={amountStr}
|
currency={limit.currency}
|
||||||
onChange={(v) => {
|
value={amountStr}
|
||||||
setAmountStr(v);
|
name="withdraw-amount"
|
||||||
}}
|
onChange={(v) => {
|
||||||
error={errors?.amount}
|
setAmountStr(v);
|
||||||
ref={ref}
|
}}
|
||||||
/>
|
error={errors?.amount}
|
||||||
</p>
|
ref={ref}
|
||||||
<p>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<input
|
<div class="sm:col-span-5">
|
||||||
id="select-exchange"
|
<span class="isolate inline-flex rounded-md shadow-sm">
|
||||||
class="pure-button pure-button-primary"
|
<button type="button"
|
||||||
type="submit"
|
class="relative inline-flex px-3 py-2 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"
|
||||||
disabled={!!errors}
|
onClick={(e) => {
|
||||||
value={i18n.str`Withdraw`}
|
e.preventDefault();
|
||||||
onClick={async (e) => {
|
setAmountStr("50.00")
|
||||||
e.preventDefault();
|
}}
|
||||||
if (!parsedAmount) return;
|
>
|
||||||
try {
|
50.00
|
||||||
const result = await createWithdrawal({
|
</button>
|
||||||
amount: Amounts.stringify(parsedAmount),
|
<button type="button"
|
||||||
});
|
class="relative -ml-px -mr-px inline-flex px-3 py-2 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
|
onClick={(e) => {
|
||||||
if (!uri) {
|
e.preventDefault();
|
||||||
return notifyError({
|
setAmountStr("25.00")
|
||||||
title: i18n.str`Server responded with an invalid withdraw URI`,
|
}}
|
||||||
description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`,
|
>
|
||||||
});
|
|
||||||
} else {
|
25.00
|
||||||
onSuccess(uri.withdrawalOperationId);
|
</button>
|
||||||
}
|
<button type="button"
|
||||||
} catch (error) {
|
class="relative -ml-px -mr-px inline-flex px-3 py-2 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
|
||||||
if (error instanceof RequestError) {
|
onClick={(e) => {
|
||||||
notifyError(
|
e.preventDefault();
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
setAmountStr("10.00")
|
||||||
onClientError: (status) =>
|
}}
|
||||||
status === HttpStatusCode.Forbidden
|
>
|
||||||
? i18n.str`The operation was rejected due to insufficient funds`
|
10.00
|
||||||
: undefined,
|
</button>
|
||||||
}),
|
<button type="button"
|
||||||
);
|
class="relative inline-flex px-3 py-2 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"
|
||||||
} else {
|
onClick={(e) => {
|
||||||
notifyError({
|
e.preventDefault();
|
||||||
title: i18n.str`Operation failed, please report`,
|
setAmountStr("5.00")
|
||||||
description:
|
}}
|
||||||
error instanceof Error
|
>
|
||||||
? error.message
|
5.00
|
||||||
: JSON.stringify(error),
|
</button>
|
||||||
});
|
</span>
|
||||||
}
|
</div>
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="flex items-center justify-between 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"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate></button>
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
// disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doStart()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Continue</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Amount(
|
// export function Amount(
|
||||||
{
|
// {
|
||||||
currency,
|
// currency,
|
||||||
value,
|
// value,
|
||||||
error,
|
// error,
|
||||||
onChange,
|
// onChange,
|
||||||
}: {
|
// }: {
|
||||||
error?: string;
|
// error?: string;
|
||||||
currency: string;
|
// currency: string;
|
||||||
value: string | undefined;
|
// value: string | undefined;
|
||||||
onChange?: (s: string) => void;
|
// onChange?: (s: string) => void;
|
||||||
},
|
// },
|
||||||
ref: Ref<HTMLInputElement>,
|
// ref: Ref<HTMLInputElement>,
|
||||||
): VNode {
|
// ): VNode {
|
||||||
return (
|
// return (
|
||||||
<div style={{ width: "max-content" }}>
|
// <div style={{ width: "max-content" }}>
|
||||||
<div>
|
// <div>
|
||||||
<input
|
// <input
|
||||||
type="text"
|
// type="text"
|
||||||
readonly
|
// readonly
|
||||||
class="currency-indicator"
|
// class="currency-indicator"
|
||||||
size={currency.length}
|
// size={currency.length}
|
||||||
maxLength={currency.length}
|
// maxLength={currency.length}
|
||||||
tabIndex={-1}
|
// tabIndex={-1}
|
||||||
style={{
|
// style={{
|
||||||
borderTopRightRadius: 0,
|
// borderTopRightRadius: 0,
|
||||||
borderBottomRightRadius: 0,
|
// borderBottomRightRadius: 0,
|
||||||
borderRight: 0,
|
// borderRight: 0,
|
||||||
}}
|
// }}
|
||||||
value={currency}
|
// value={currency}
|
||||||
/>
|
// />
|
||||||
<input
|
// <input
|
||||||
type="number"
|
// type="number"
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
name="amount"
|
// name="amount"
|
||||||
id="amount"
|
// id="amount"
|
||||||
placeholder="0"
|
// placeholder="0"
|
||||||
style={{
|
// style={{
|
||||||
borderTopLeftRadius: 0,
|
// borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: 0,
|
// borderBottomLeftRadius: 0,
|
||||||
borderLeft: 0,
|
// borderLeft: 0,
|
||||||
width: 150,
|
// width: 150,
|
||||||
color: "black",
|
// color: "black",
|
||||||
}}
|
// }}
|
||||||
value={value ?? ""}
|
// value={value ?? ""}
|
||||||
disabled={!onChange}
|
// disabled={!onChange}
|
||||||
onInput={(e): void => {
|
// onInput={(e): void => {
|
||||||
if (onChange) {
|
// if (onChange) {
|
||||||
onChange(e.currentTarget.value);
|
// onChange(e.currentTarget.value);
|
||||||
}
|
// }
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<ShowInputErrorLabel message={error} isDirty={value !== undefined} />
|
// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
@ -15,26 +15,40 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
|
PaytoUri,
|
||||||
|
PaytoUriGeneric,
|
||||||
|
PaytoUriIBAN,
|
||||||
|
PaytoUriTalerBank,
|
||||||
|
TranslatedString,
|
||||||
WithdrawUriResult,
|
WithdrawUriResult,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
RequestError,
|
RequestError,
|
||||||
|
notify,
|
||||||
|
notifyError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
import { useAccessAnonAPI } from "../hooks/access.js";
|
import { useAccessAnonAPI } from "../hooks/access.js";
|
||||||
import { notifyError } from "../hooks/notification.js";
|
|
||||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
|
import { Amount } from "./PaytoWireTransferForm.js";
|
||||||
|
|
||||||
const logger = new Logger("WithdrawalConfirmationQuestion");
|
const logger = new Logger("WithdrawalConfirmationQuestion");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onAborted: () => void;
|
onAborted: () => void;
|
||||||
withdrawUri: WithdrawUriResult;
|
withdrawUri: WithdrawUriResult;
|
||||||
|
details: {
|
||||||
|
account: PaytoUri,
|
||||||
|
reserve: string,
|
||||||
|
amount: AmountJson,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Additional authentication required to complete the operation.
|
* Additional authentication required to complete the operation.
|
||||||
@ -42,6 +56,7 @@ interface Props {
|
|||||||
*/
|
*/
|
||||||
export function WithdrawalConfirmationQuestion({
|
export function WithdrawalConfirmationQuestion({
|
||||||
onAborted,
|
onAborted,
|
||||||
|
details,
|
||||||
withdrawUri,
|
withdrawUri,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -60,135 +75,257 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
answer: !captchaAnswer
|
answer: !captchaAnswer
|
||||||
? i18n.str`Answer the question before continue`
|
? i18n.str`Answer the question before continue`
|
||||||
: Number.isNaN(answer)
|
: Number.isNaN(answer)
|
||||||
? i18n.str`The answer should be a number`
|
? i18n.str`The answer should be a number`
|
||||||
: answer !== captchaNumbers.a + captchaNumbers.b
|
: answer !== captchaNumbers.a + captchaNumbers.b
|
||||||
? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
|
? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function doTransfer() {
|
||||||
|
try {
|
||||||
|
await confirmWithdrawal(
|
||||||
|
withdrawUri.withdrawalOperationId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
|
||||||
|
: status === HttpStatusCode.UnprocessableEntity
|
||||||
|
? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
try {
|
||||||
|
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
||||||
|
onAborted();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
|
<div class="bg-white shadow sm:rounded-lg">
|
||||||
<article>
|
<div class="px-4 py-5 sm:p-6">
|
||||||
<div class="challenge-div">
|
<h3 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
<form
|
<i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
|
||||||
class="challenge-form"
|
</h3>
|
||||||
noValidate
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
onSubmit={(e) => {
|
<div class="px-4 mt-4 ">
|
||||||
e.preventDefault();
|
<div class="w-full">
|
||||||
}}
|
<div class="px-4 sm:px-0">
|
||||||
autoCapitalize="none"
|
<p><i18n.Translate>Wire transfer details</i18n.Translate></p>
|
||||||
autoCorrect="off"
|
</div>
|
||||||
>
|
<div class="mt-6 border-t border-gray-100">
|
||||||
<div class="pure-form" id="captcha" name="capcha-form">
|
<dl class="divide-y divide-gray-100">
|
||||||
<h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
|
{((): VNode => {
|
||||||
<p>
|
switch (details.account.targetType) {
|
||||||
<label for="answer">
|
case "iban": {
|
||||||
{i18n.str`What is`}
|
const p = details.account as PaytoUriIBAN
|
||||||
<em>
|
const name = p.params["receiver-name"]
|
||||||
{captchaNumbers.a} + {captchaNumbers.b}
|
return <Fragment>
|
||||||
</em>
|
<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">Exchange account</dt>
|
||||||
</label>
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
|
||||||
|
</div>
|
||||||
<input
|
{name &&
|
||||||
name="answer"
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
id="answer"
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
|
||||||
value={captchaAnswer ?? ""}
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
|
||||||
type="text"
|
</div>
|
||||||
autoFocus
|
}
|
||||||
required
|
</Fragment>
|
||||||
onInput={(e): void => {
|
}
|
||||||
setCaptchaAnswer(e.currentTarget.value);
|
case "x-taler-bank": {
|
||||||
}}
|
const p = details.account as PaytoUriTalerBank
|
||||||
/>
|
const name = p.params["receiver-name"]
|
||||||
<ShowInputErrorLabel
|
return <Fragment>
|
||||||
message={errors?.answer}
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
isDirty={captchaAnswer !== undefined}
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
|
||||||
/>
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
{name &&
|
||||||
<button
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
type="submit"
|
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
|
||||||
class="pure-button pure-button-primary btn-confirm"
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
|
||||||
disabled={!!errors}
|
</div>
|
||||||
onClick={async (e) => {
|
}
|
||||||
e.preventDefault();
|
</Fragment>
|
||||||
try {
|
}
|
||||||
await confirmWithdrawal(
|
default:
|
||||||
withdrawUri.withdrawalOperationId,
|
return <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">Exchange account</dt>
|
||||||
} catch (error) {
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
|
||||||
if (error instanceof RequestError) {
|
</div>
|
||||||
notifyError(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
onClientError: (status) =>
|
|
||||||
status === HttpStatusCode.Conflict
|
|
||||||
? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
|
|
||||||
: status === HttpStatusCode.UnprocessableEntity
|
|
||||||
? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notifyError({
|
|
||||||
title: i18n.str`Operation failed, please report`,
|
|
||||||
description:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
})()}
|
||||||
}}
|
<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>
|
||||||
{i18n.str`Confirm`}
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.reserve}</dd>
|
||||||
</button>
|
</div>
|
||||||
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
<button
|
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
||||||
class="pure-button pure-button-secondary btn-cancel"
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd>
|
||||||
onClick={async (e) => {
|
</div>
|
||||||
e.preventDefault();
|
</dl>
|
||||||
try {
|
</div>
|
||||||
await abortWithdrawal(withdrawUri.withdrawalOperationId);
|
</div>
|
||||||
onAborted();
|
|
||||||
} catch (error) {
|
</div>
|
||||||
if (error instanceof RequestError) {
|
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-3">
|
||||||
notifyError(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}>
|
||||||
onClientError: (status) =>
|
<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" />
|
||||||
status === HttpStatusCode.Conflict
|
<span class="flex flex-1">
|
||||||
? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
|
<span class="flex flex-col">
|
||||||
: undefined,
|
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
|
||||||
}),
|
<i18n.Translate>challenge response test</i18n.Translate>
|
||||||
);
|
</span>
|
||||||
} else {
|
</span>
|
||||||
notifyError({
|
</span>
|
||||||
title: i18n.str`Operation failed, please report`,
|
<svg class="h-5 w-5 text-indigo-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
description:
|
<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" />
|
||||||
error instanceof Error
|
</svg>
|
||||||
? error.message
|
</label>
|
||||||
: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
<label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
|
||||||
}}
|
<span class="flex flex-1">
|
||||||
>
|
<span class="flex flex-col">
|
||||||
{i18n.str`Cancel`}
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
</button>
|
<i18n.Translate>using SMS</i18n.Translate>
|
||||||
</p>
|
</span>
|
||||||
|
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
|
<i18n.Translate>not available</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
|
||||||
|
<span class="flex flex-1">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
|
<i18n.Translate>one time password</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
|
<i18n.Translate>not available</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-sm leading-6">
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
|
<div class="px-4 sm:px-0">
|
||||||
|
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>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"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6 sm:p-8">
|
||||||
|
<label for="withdraw-amount">{i18n.str`What is`}
|
||||||
|
<em>
|
||||||
|
{captchaNumbers.a} + {captchaNumbers.b}
|
||||||
|
</em>
|
||||||
|
?
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
// class="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"
|
||||||
|
aria-describedby="answer"
|
||||||
|
autoFocus
|
||||||
|
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"
|
||||||
|
// value={username ?? ""}
|
||||||
|
required
|
||||||
|
|
||||||
|
name="answer"
|
||||||
|
id="answer"
|
||||||
|
autocomplete="off"
|
||||||
|
// value={value ?? ""}
|
||||||
|
// disabled={!onChange}
|
||||||
|
// onInput={(e): void => {
|
||||||
|
// if (onChange) {
|
||||||
|
// onChange(e.currentTarget.value);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ShowInputErrorLabel message={errors?.answer} isDirty={false} />
|
||||||
|
</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">
|
||||||
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
// onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate></button>
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
// disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault()
|
||||||
|
// doStart()
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Transfer</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<div class="hint">
|
|
||||||
<p>
|
|
||||||
<i18n.Translate>
|
|
||||||
A this point, a <b>real</b> bank would ask for an additional
|
|
||||||
authentication proof (PIN/TAN, one time password, ..), instead
|
|
||||||
of a simple calculation.
|
|
||||||
</i18n.Translate>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Amounts,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
Logger,
|
Logger,
|
||||||
WithdrawUriResult,
|
WithdrawUriResult,
|
||||||
|
parsePaytoUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { useWithdrawalDetails } from "../hooks/access.js";
|
import { useWithdrawalDetails } from "../hooks/access.js";
|
||||||
import { notifyInfo } from "../hooks/notification.js";
|
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { handleNotOkResult } from "./HomePage.js";
|
import { handleNotOkResult } from "./HomePage.js";
|
||||||
import { QrCodeSection } from "./QrCodeSection.js";
|
import { QrCodeSection } from "./QrCodeSection.js";
|
||||||
@ -127,6 +128,19 @@ export function WithdrawalQRCode({
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
if (!data.selected_reserve_pub) {
|
||||||
|
return <div>
|
||||||
|
the exchange is selcted but no reserve pub
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return <div>
|
||||||
|
the exchange is selcted but no account
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.selection_done) {
|
if (!data.selection_done) {
|
||||||
return (
|
return (
|
||||||
@ -144,6 +158,11 @@ export function WithdrawalQRCode({
|
|||||||
return (
|
return (
|
||||||
<WithdrawalConfirmationQuestion
|
<WithdrawalConfirmationQuestion
|
||||||
withdrawUri={withdrawUri}
|
withdrawUri={withdrawUri}
|
||||||
|
details={{
|
||||||
|
account,
|
||||||
|
reserve: data.selected_reserve_pub,
|
||||||
|
amount: Amounts.parseOrThrow("usd:10.00")
|
||||||
|
}}
|
||||||
onAborted={() => {
|
onAborted={() => {
|
||||||
notifyInfo(i18n.str`Operation canceled`);
|
notifyInfo(i18n.str`Operation canceled`);
|
||||||
clearCurrentWithdrawal()
|
clearCurrentWithdrawal()
|
||||||
|
2890
packages/demobank-ui/src/pages/rnd.ts
Normal file
2890
packages/demobank-ui/src/pages/rnd.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,11 +16,12 @@
|
|||||||
|
|
||||||
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
|
ErrorNotification,
|
||||||
ErrorType,
|
ErrorType,
|
||||||
HttpError,
|
HttpError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { ErrorMessage } from "./hooks/notification.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate (the number part of) an amount. If needed,
|
* Validate (the number part of) an amount. If needed,
|
||||||
@ -120,11 +121,12 @@ export function buildRequestErrorMessage(
|
|||||||
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
||||||
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
|
||||||
} = {},
|
} = {},
|
||||||
): ErrorMessage {
|
): ErrorNotification {
|
||||||
let result: ErrorMessage;
|
let result: ErrorNotification;
|
||||||
switch (cause.type) {
|
switch (cause.type) {
|
||||||
case ErrorType.TIMEOUT: {
|
case ErrorType.TIMEOUT: {
|
||||||
result = {
|
result = {
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Request timeout`,
|
title: i18n.str`Request timeout`,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@ -133,8 +135,9 @@ export function buildRequestErrorMessage(
|
|||||||
const title =
|
const title =
|
||||||
specialCases.onClientError && specialCases.onClientError(cause.status);
|
specialCases.onClientError && specialCases.onClientError(cause.status);
|
||||||
result = {
|
result = {
|
||||||
|
type: "error",
|
||||||
title: title ? title : i18n.str`The server didn't accept the request`,
|
title: title ? title : i18n.str`The server didn't accept the request`,
|
||||||
description: cause?.payload?.error?.description,
|
description: cause?.payload?.error?.description as TranslatedString,
|
||||||
debug: JSON.stringify(cause),
|
debug: JSON.stringify(cause),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@ -143,24 +146,27 @@ export function buildRequestErrorMessage(
|
|||||||
const title =
|
const title =
|
||||||
specialCases.onServerError && specialCases.onServerError(cause.status);
|
specialCases.onServerError && specialCases.onServerError(cause.status);
|
||||||
result = {
|
result = {
|
||||||
|
type: "error",
|
||||||
title: title
|
title: title
|
||||||
? title
|
? title
|
||||||
: i18n.str`The server had problems processing the request`,
|
: i18n.str`The server had problems processing the request`,
|
||||||
description: cause?.payload?.error?.description,
|
description: cause?.payload?.error?.description as TranslatedString,
|
||||||
debug: JSON.stringify(cause),
|
debug: JSON.stringify(cause),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.UNREADABLE: {
|
case ErrorType.UNREADABLE: {
|
||||||
result = {
|
result = {
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Unexpected error`,
|
title: i18n.str`Unexpected error`,
|
||||||
description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`,
|
description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString,
|
||||||
debug: JSON.stringify(cause),
|
debug: JSON.stringify(cause),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ErrorType.UNEXPECTED: {
|
case ErrorType.UNEXPECTED: {
|
||||||
result = {
|
result = {
|
||||||
|
type: "error",
|
||||||
title: i18n.str`Unexpected error`,
|
title: i18n.str`Unexpected error`,
|
||||||
debug: JSON.stringify(cause),
|
debug: JSON.stringify(cause),
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
lockfileVersion: '6.1'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
@ -877,6 +877,9 @@ importers:
|
|||||||
'@gnu-taler/taler-util':
|
'@gnu-taler/taler-util':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../taler-util
|
version: link:../taler-util
|
||||||
|
'@heroicons/react':
|
||||||
|
specifier: ^2.0.17
|
||||||
|
version: 2.0.17(react@18.2.0)
|
||||||
'@linaria/babel-preset':
|
'@linaria/babel-preset':
|
||||||
specifier: 4.4.5
|
specifier: 4.4.5
|
||||||
version: 4.4.5
|
version: 4.4.5
|
||||||
@ -907,6 +910,9 @@ importers:
|
|||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
|
date-fns:
|
||||||
|
specifier: 2.29.3
|
||||||
|
version: 2.29.3
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.17.7
|
specifier: ^0.17.7
|
||||||
version: 0.17.7
|
version: 0.17.7
|
||||||
@ -4872,7 +4878,6 @@ packages:
|
|||||||
react: '>= 16'
|
react: '>= 16'
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.11:
|
/@humanwhocodes/config-array@0.11.11:
|
||||||
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
|
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
|
||||||
@ -8680,7 +8685,6 @@ packages:
|
|||||||
/date-fns@2.29.3:
|
/date-fns@2.29.3:
|
||||||
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||||
engines: {node: '>=0.11'}
|
engines: {node: '>=0.11'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/date-time@3.1.0:
|
/date-time@3.1.0:
|
||||||
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
|
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
|
||||||
|
Loading…
Reference in New Issue
Block a user