aboutsummaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/PaytoWireTransferForm.tsx')
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx304
1 files changed, 174 insertions, 130 deletions
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 8d9df1151..d10f62cce 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -29,7 +29,7 @@ import {
assertUnreachable,
buildPayto,
parsePaytoUri,
- stringifyPaytoUri
+ stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
InternationalizationAPI,
@@ -43,9 +43,9 @@ import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
-import { useSessionState } from "../hooks/session.js";
import { useBankState } from "../hooks/bank-state.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
interface Props {
@@ -59,9 +59,9 @@ interface Props {
routeCancel?: RouteDefinition;
routeCashout?: RouteDefinition;
routeHere: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
limit: AmountJson;
balance: AmountJson;
@@ -79,7 +79,6 @@ export function PaytoWireTransferForm({
routeHere,
onAuthorizationRequired,
limit,
- balance,
}: Props): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
const { state: credentials } = useSessionState();
@@ -101,14 +100,19 @@ export function PaytoWireTransferForm({
const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
const [notification, notify, handleError] = useLocalNotification();
- const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const;
+ const paytoType =
+ config.wire_type === "X_TALER_BANK"
+ ? ("x-taler-bank" as const)
+ : ("iban" as const);
const errorsWire = undefinedIfEmpty({
account: !account
? i18n.str`Required`
- : paytoType === "iban" ? validateIBAN(account, i18n) :
- paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) :
- undefined,
+ : paytoType === "iban"
+ ? validateIBAN(account, i18n)
+ : paytoType === "x-taler-bank"
+ ? validateTalerBank(account, i18n)
+ : undefined,
subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
amount: !trimmedAmountStr
? i18n.str`Required`
@@ -119,11 +123,11 @@ export function PaytoWireTransferForm({
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
-
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n.str`Required`
- : !parsed ? i18n.str`Does not follow the pattern`
+ : !parsed
+ ? i18n.str`Does not follow the pattern`
: validateRawPayto(parsed, limit, url.host, i18n, paytoType),
});
@@ -140,11 +144,15 @@ export function PaytoWireTransferForm({
delete p.params.amount;
// if this payto is valid then it already have message
payto_uri = stringifyPaytoUri(p);
- acName = !p.isKnown ? undefined :
- p.targetType === "iban" ? p.iban :
- p.targetType === "bitcoin" ? p.targetPath :
- p.targetType === "x-taler-bank" ? p.account :
- assertUnreachable(p);
+ acName = !p.isKnown
+ ? undefined
+ : p.targetType === "iban"
+ ? p.iban
+ : p.targetType === "bitcoin"
+ ? p.targetPath
+ : p.targetType === "x-taler-bank"
+ ? p.account
+ : assertUnreachable(p);
} else {
if (!account || !subject) return;
let payto;
@@ -159,7 +167,8 @@ export function PaytoWireTransferForm({
payto = buildPayto("iban", account, undefined);
break;
}
- default: assertUnreachable(paytoType)
+ default:
+ assertUnreachable(paytoType);
}
payto.params.message = encodeURIComponent(subject);
@@ -184,6 +193,7 @@ export function PaytoWireTransferForm({
title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Unauthorized:
return notify({
@@ -191,13 +201,25 @@ export function PaytoWireTransferForm({
title: i18n.str`Not enough permission to complete the operation.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank administrator can't be the transfer creditor.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
return notify({
type: "error",
- title: i18n.str`The destination account "${acName ?? puri}" was not found.`,
+ title: i18n.str`The destination account "${
+ acName ?? puri
+ }" was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_SAME_ACCOUNT:
return notify({
@@ -205,6 +227,7 @@ export function PaytoWireTransferForm({
title: i18n.str`The origin and the destination of the transfer can't be the same.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -212,6 +235,7 @@ export function PaytoWireTransferForm({
title: i18n.str`Your balance is not enough.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -219,12 +243,17 @@ export function PaytoWireTransferForm({
title: i18n.str`The origin account "${puri}" was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "create-transaction",
id: String(resp.body.challenge_id),
- location: routeHere.url({ account: account ?? "", amount, subject }),
+ location: routeHere.url({
+ account: account ?? "",
+ amount,
+ subject,
+ }),
sent: AbsoluteTime.never(),
request,
});
@@ -281,10 +310,12 @@ export function PaytoWireTransferForm({
break;
}
default: {
- assertUnreachable(parsed)
+ assertUnreachable(parsed);
}
}
- const amountStr = !parsed.params ? undefined : parsed.params["amount"];
+ const amountStr = !parsed.params
+ ? undefined
+ : parsed.params["amount"];
if (amountStr) {
const amount = Amounts.parse(amountStr);
if (amount) {
@@ -350,7 +381,8 @@ export function PaytoWireTransferForm({
}
break;
}
- default: assertUnreachable(paytoType)
+ default:
+ assertUnreachable(paytoType);
}
rawPaytoInputSetter(stringifyPaytoUri(payto));
}
@@ -374,9 +406,7 @@ export function PaytoWireTransferForm({
>
<i18n.Translate>Cashout</i18n.Translate>
</a>
- ) : (
- undefined
- )}
+ ) : undefined}
</div>
</div>
@@ -394,34 +424,39 @@ export function PaytoWireTransferForm({
{(() => {
switch (paytoType) {
case "x-taler-bank": {
- return <TextField
- id="x-taler-bank"
- required
- label={i18n.str`Recipient`}
- help={i18n.str`Id of the recipient's account`}
- error={errorsWire?.account}
- onChange={setAccount}
- value={account}
- placeholder={i18n.str`username`}
- focus={focus}
- disabled={sendingToFixedAccount}
- />
+ return (
+ <TextField
+ id="x-taler-bank"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`Id of the recipient's account`}
+ error={errorsWire?.account}
+ onChange={setAccount}
+ value={account}
+ placeholder={i18n.str`username`}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
}
case "iban": {
- return <TextField
- id="iban"
- required
- label={i18n.str`Recipient`}
- help={i18n.str`IBAN of the recipient's account`}
- placeholder={"CC0123456789" as TranslatedString}
- error={errorsWire?.account}
- onChange={(v) => setAccount(v.toUpperCase())}
- value={account}
- focus={focus}
- disabled={sendingToFixedAccount}
- />
+ return (
+ <TextField
+ id="iban"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`IBAN of the recipient's account`}
+ placeholder={"CC0123456789" as TranslatedString}
+ error={errorsWire?.account}
+ onChange={(v) => setAccount(v.toUpperCase())}
+ value={account}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
}
- default: assertUnreachable(paytoType)
+ default:
+ assertUnreachable(paytoType);
}
})()}
@@ -506,11 +541,12 @@ export function PaytoWireTransferForm({
value={rawPaytoInput ?? ""}
required
title={i18n.str`Uniform resource identifier of the target account`}
-
placeholder={((): TranslatedString => {
switch (paytoType) {
- case "x-taler-bank": return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`
- case "iban": return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`
+ case "x-taler-bank":
+ return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`;
+ case "iban":
+ return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`;
}
})()}
onInput={(e): void => {
@@ -618,13 +654,13 @@ export function InputAmount(
if (
sep_pos !== -1 &&
l - sep_pos - 1 >
- config.currency_specification.num_fractional_input_digits
+ config.currency_specification.num_fractional_input_digits
) {
e.currentTarget.value = e.currentTarget.value.substring(
0,
sep_pos +
- config.currency_specification.num_fractional_input_digits +
- 1,
+ config.currency_specification.num_fractional_input_digits +
+ 1,
);
}
onChange(e.currentTarget.value);
@@ -668,81 +704,94 @@ export function RenderAmount({
);
}
-
-function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString | undefined {
+function validateRawPayto(
+ parsed: PaytoUri,
+ limit: AmountJson,
+ host: string,
+ i18n: InternationalizationAPI,
+ type: "iban" | "x-taler-bank",
+): TranslatedString | undefined {
if (!parsed.isKnown) {
- return i18n.str`The target type is unknown, use "${type}"`
+ return i18n.str`The target type is unknown, use "${type}"`;
}
let result: TranslatedString | undefined;
switch (type) {
case "x-taler-bank": {
if (parsed.targetType !== "x-taler-bank") {
- return i18n.str`Only "x-taler-bank" target are supported`
+ return i18n.str`Only "x-taler-bank" target are supported`;
}
if (parsed.host !== host) {
- return i18n.str`Only this host is allowed. Use "${host}"`
+ return i18n.str`Only this host is allowed. Use "${host}"`;
}
if (!parsed.account) {
- return i18n.str`Missing account name`
+ return i18n.str`Missing account name`;
}
- const result = validateTalerBank(parsed.account, i18n)
- if (result) return result
+ const result = validateTalerBank(parsed.account, i18n);
+ if (result) return result;
break;
}
case "iban": {
if (parsed.targetType !== "iban") {
- return i18n.str`Only "IBAN" target are supported`
+ return i18n.str`Only "IBAN" target are supported`;
}
- const result = validateIBAN(parsed.iban, i18n)
- if (result) return result
+ const result = validateIBAN(parsed.iban, i18n);
+ if (result) return result;
break;
}
- default: assertUnreachable(type)
+ default:
+ assertUnreachable(type);
}
if (!parsed.params.amount) {
- return i18n.str`Missing "amount" parameter to specify the amount to be transferred`
+ return i18n.str`Missing "amount" parameter to specify the amount to be transferred`;
}
- const amount = Amounts.parse(parsed.params.amount)
+ const amount = Amounts.parse(parsed.params.amount);
if (!amount) {
- return i18n.str`The "amount" parameter is not valid`
+ return i18n.str`The "amount" parameter is not valid`;
}
- result = validateAmount(amount, limit, i18n)
+ result = validateAmount(amount, limit, i18n);
if (result) return result;
if (!parsed.params.message) {
- return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`
+ return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`;
}
- const subject = parsed.params.message
- result = validateSubject(subject, i18n)
+ const subject = parsed.params.message;
+ result = validateSubject(subject, i18n);
if (result) return result;
- return undefined
+ return undefined;
}
-function validateAmount(amount: AmountJson, limit: AmountJson, i18n: InternationalizationAPI): TranslatedString | undefined {
+function validateAmount(
+ amount: AmountJson,
+ limit: AmountJson,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
if (amount.currency !== limit.currency) {
- return i18n.str`The only currency allowed is "${limit.currency}"`
+ return i18n.str`The only currency allowed is "${limit.currency}"`;
}
if (Amounts.isZero(amount)) {
- return i18n.str`Can't transfer zero amount`
+ return i18n.str`Can't transfer zero amount`;
}
if (Amounts.cmp(limit, amount) === -1) {
- return i18n.str`Balance is not enough`
+ return i18n.str`Balance is not enough`;
}
- return undefined
+ return undefined;
}
-function validateSubject(text: string, i18n: InternationalizationAPI): TranslatedString | undefined {
+function validateSubject(
+ text: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
if (text.length < 2) {
- return i18n.str`Use a longer subject`
+ return i18n.str`Use a longer subject`;
}
- return undefined
+ return undefined;
}
interface PaytoFieldProps {
- id: string,
+ id: string;
label: TranslatedString;
required?: boolean;
help?: TranslatedString;
@@ -755,13 +804,17 @@ interface PaytoFieldProps {
disabled?: boolean;
}
-function Wrapper({ withIcon, children }: { withIcon: boolean, children: ComponentChildren }): VNode {
+function Wrapper({
+ withIcon,
+ children,
+}: {
+ withIcon: boolean;
+ children: ComponentChildren;
+}): VNode {
if (withIcon) {
- return <div class="flex justify-between">
- {children}
- </div>
+ return <div class="flex justify-between">{children}</div>;
}
- return <Fragment>{children}</Fragment>
+ return <Fragment>{children}</Fragment>;
}
export function TextField({
@@ -777,43 +830,34 @@ export function TextField({
value,
error,
}: PaytoFieldProps): VNode {
- return <div class="sm:col-span-5">
- <label
- for={id}
- class="block text-sm font-medium leading-6 text-gray-900"
- >{label}
- {required &&
- <b style={{ color: "red" }}> *</b>
- }
- </label>
- <div class="mt-2">
- <Wrapper withIcon={rightIcons !== undefined}>
- <input
- ref={focus ? doAutoFocus : undefined}
- type="text"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name={id}
- id={id}
- disabled={disabled}
- value={value ?? ""}
- placeholder={placeholder}
- autocomplete="off"
- required
- onInput={(e): void => {
- onChange(e.currentTarget.value);
- }}
- />
- {rightIcons}
- </Wrapper>
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
+ return (
+ <div class="sm:col-span-5">
+ <label for={id} class="block text-sm font-medium leading-6 text-gray-900">
+ {label}
+ {required && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <Wrapper withIcon={rightIcons !== undefined}>
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={id}
+ id={id}
+ disabled={disabled}
+ value={value ?? ""}
+ placeholder={placeholder}
+ autocomplete="off"
+ required
+ onInput={(e): void => {
+ onChange(e.currentTarget.value);
+ }}
+ />
+ {rightIcons}
+ </Wrapper>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ </div>
+ {help && <p class="mt-2 text-sm text-gray-500">{help}</p>}
</div>
- {help &&
- <p class="mt-2 text-sm text-gray-500">
- {help}
- </p>
- }
- </div>
+ );
}