diff options
Diffstat (limited to 'packages/bank-ui/src/pages/PaytoWireTransferForm.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 304 |
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> + ); } |