From 4eda6ac07c78bcb3c2daa7846b4cd36048f9c7dd Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 6 Feb 2024 16:51:15 -0300 Subject: support for x-taler-bank and fix cache invalidation when new account is created --- packages/demobank-ui/src/pages/PaymentOptions.tsx | 15 +- .../src/pages/PaytoWireTransferForm.tsx | 357 ++++++++++---- .../demobank-ui/src/pages/SolveChallengePage.tsx | 6 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 536 +++++++++++---------- .../demobank-ui/src/pages/admin/AccountList.tsx | 24 +- packages/demobank-ui/src/pages/admin/AdminHome.tsx | 6 +- .../src/pages/admin/CreateNewAccount.tsx | 2 - .../src/pages/business/CreateCashout.tsx | 23 +- .../src/pages/business/ShowCashoutDetails.tsx | 12 +- 9 files changed, 592 insertions(+), 389 deletions(-) (limited to 'packages/demobank-ui/src/pages') diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 39b31a094..a508845e1 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -33,18 +33,19 @@ function ShowOperationPendingTag({ }): VNode { const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(woid); + const loading = !result const error = - !result || result instanceof TalerError || result.type === "fail"; - const completed = - !error && - (result.body.status === "aborted" || result.body.status === "confirmed"); + !loading && (result instanceof TalerError || result.type === "fail"); + const pending = + !loading && !error && + (result.body.status === "pending" || result.body.status === "selected"); useEffect(() => { - if (completed && onOperationAlreadyCompleted) { + if (!loading && !pending && onOperationAlreadyCompleted) { onOperationAlreadyCompleted(); } - }, [completed]); + }, [pending]); - if (error || completed) { + if (error || !pending) { return ; } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 3643e1f6b..54ceb81a9 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -23,28 +23,30 @@ import { FRAC_SEPARATOR, HttpStatusCode, PaytoString, + PaytoUri, TalerErrorCode, TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, - stringifyPaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { + InternationalizationAPI, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Ref, VNode, h } from "preact"; +import { ComponentChildren, Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { useBankState } from "../hooks/bank-state.js"; import { RouteDefinition } from "../route.js"; -import { undefinedIfEmpty, validateIBAN } from "../utils.js"; +import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js"; export function PaytoWireTransferForm({ focus, @@ -65,11 +67,11 @@ export function PaytoWireTransferForm({ }): VNode { const [isRawPayto, setIsRawPayto] = useState(false); const { state: credentials } = useBackendState(); - const { api } = useBankCoreApiContext(); + const { api, config, url } = useBankCoreApiContext(); const sendingToFixedAccount = toAccount !== undefined; - // FIXME: support other destination that just IBAN - const [iban, setIban] = useState(toAccount); + + const [account, setAccount] = useState(toAccount); const [subject, setSubject] = useState(); const [amount, setAmount] = useState(); const [, updateBankState] = useBankState(); @@ -78,49 +80,35 @@ export function PaytoWireTransferForm({ undefined, ); const { i18n } = useTranslationContext(); - const ibanRegex = "^[A-Z][A-Z][0-9]+$"; const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); - const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const [notification, notify, handleError] = useLocalNotification(); + const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const; + const errorsWire = undefinedIfEmpty({ - iban: !iban + account: !account ? i18n.str`Required` - : !IBAN_REGEX.test(iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(iban, i18n), - subject: !subject ? i18n.str`Required` : 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` : !parsedAmount ? i18n.str`Not valid` - : Amounts.isZero(parsedAmount) - ? i18n.str`Should be greater than 0` - : Amounts.cmp(limit, parsedAmount) === -1 - ? i18n.str`Balance is not enough` - : undefined, + : validateAmount(parsedAmount, limit, i18n), }); const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput); + const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n.str`Required` - : !parsed - ? i18n.str`Does not follow the pattern` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`Only "IBAN" target are supported` - : !parsed.params.amount - ? i18n.str`Use the "amount" parameter to specify the amount to be transferred` - : Amounts.parse(parsed.params.amount) === undefined - ? i18n.str`The amount is not valid` - : !parsed.params.message - ? i18n.str`Use the "message" parameter to specify a reference text for the transfer` - : !IBAN_REGEX.test(parsed.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(parsed.iban, i18n), + : !parsed ? i18n.str`Does not follow the pattern` + : validateRawPayto(parsed, limit, url.host, i18n, paytoType), }); async function doSend() { @@ -128,18 +116,30 @@ export function PaytoWireTransferForm({ let sendingAmount: AmountString | undefined; if (credentials.status !== "loggedIn") return; - if (rawPaytoInput) { - const p = parsePaytoUri(rawPaytoInput); + if (isRawPayto) { + const p = parsePaytoUri(rawPaytoInput!); if (!p) return; sendingAmount = p.params.amount as AmountString; delete p.params.amount; // if this payto is valid then it already have message payto_uri = stringifyPaytoUri(p); } else { - if (!iban || !subject) return; - const ibanPayto = buildPayto("iban", iban, undefined); - ibanPayto.params.message = encodeURIComponent(subject); - payto_uri = stringifyPaytoUri(ibanPayto); + if (!account || !subject) return; + let payto; + switch (paytoType) { + case "x-taler-bank": { + payto = buildPayto("x-taler-bank", url.host, account); + break; + } + case "iban": { + payto = buildPayto("iban", account, undefined); + break; + } + default: assertUnreachable(paytoType) + } + + payto.params.message = encodeURIComponent(subject); + payto_uri = stringifyPaytoUri(payto); sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString; } const puri = payto_uri; @@ -212,7 +212,7 @@ export function PaytoWireTransferForm({ notifyInfo(i18n.str`Wire transfer created!`); onSuccess(); setAmount(undefined); - setIban(undefined); + setAccount(undefined); setSubject(undefined); rawPaytoInputSetter(undefined); }); @@ -243,13 +243,24 @@ export function PaytoWireTransferForm({ aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { - if ( - parsed && - parsed.isKnown && - parsed.targetType === "iban" - ) { - setIban(parsed.iban); - const amountStr = parsed.params["amount"]; + if (parsed && parsed.isKnown) { + switch (parsed.targetType) { + case "iban": { + setAccount(parsed.iban); + break; + } + case "x-taler-bank": { + setAccount(parsed.account); + break; + } + case "bitcoin": { + break; + } + default: { + assertUnreachable(parsed) + } + } + const amountStr = parsed.params["amount"] ?? `${config.currency}:0`; if (amountStr) { const amount = Amounts.parse(parsed.params["amount"]); if (amount) { @@ -290,14 +301,32 @@ export function PaytoWireTransferForm({ aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => { - if (iban) { - const payto = buildPayto("iban", iban, undefined); - if (parsedAmount) { - payto.params["amount"] = - Amounts.stringify(parsedAmount); - } - if (subject) { - payto.params["message"] = subject; + if (account) { + let payto; + switch (paytoType) { + case "x-taler-bank": { + payto = buildPayto("x-taler-bank", url.host, account); + if (parsedAmount) { + payto.params["amount"] = + Amounts.stringify(parsedAmount); + } + if (subject) { + payto.params["message"] = subject; + } + break; + } + case "iban": { + payto = buildPayto("iban", account, undefined); + if (parsedAmount) { + payto.params["amount"] = + Amounts.stringify(parsedAmount); + } + if (subject) { + payto.params["message"] = subject; + } + break; + } + default: assertUnreachable(paytoType) } rawPaytoInputSetter(stringifyPaytoUri(payto)); } @@ -328,39 +357,37 @@ export function PaytoWireTransferForm({
{!isRawPayto ? (
-
- -
- { - setIban(e.currentTarget.value.toUpperCase()); - }} - /> - -
-

- - IBAN of the recipient's account - -

-
+ {(() => { + switch (paytoType) { + case "x-taler-bank": { + return + } + case "iban": { + return setAccount(v.toUpperCase())} + value={account} + focus={focus} + disabled={sendingToFixedAccount} + /> + } + default: assertUnreachable(paytoType) + } + })()}

- Account identification + Account id for authentication

@@ -366,22 +370,26 @@ export function AccountForm({

- { form.payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} + rightIcons={ form.payto_uri ?? defaultValue.payto_uri ?? ""} + />} + value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString} + disabled={!editableAccount} />
@@ -411,6 +419,9 @@ export function AccountForm({ isDirty={form.email !== undefined} />
+

+ To be used when second factor authentication is enabled +

@@ -440,102 +451,26 @@ export function AccountForm({ isDirty={form.phone !== undefined} />
+

+ To be used when second factor authentication is enabled +

- {showingCurrentUserInfo && isCashoutEnabled && ( - { form.cashout_payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} + value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString} + disabled={!editableCashout} /> )} -
- - { - form.debit_threshold = e as AmountString; - updateForm(structuredClone(form)); - } - } - /> - -

- - How much is user able to transfer after zero balance - -

-
- - {purpose !== "create" || !userIsAdmin ? undefined : ( -
-
- - - Is this a payment provider? - - - -
-
- )} {/* channel, not shown if old cashout api */} {OLD_CASHOUT_API || config.supported_tan_channels.length === 0 ? undefined : ( @@ -584,7 +519,7 @@ export function AccountForm({ {purpose !== "show" && !hasEmail && - i18n.str`Add a email in your profile to enable this option`} + i18n.str`Add an email in your profile to enable this option`} ({ )} +
+ + { + form.debit_threshold = e as AmountString; + updateForm(structuredClone(form)); + } + } + /> + +

+ How much the balance can go below zero. +

+
+
@@ -703,11 +670,51 @@ export function AccountForm({

- - Public accounts have their balance publicly accessible - + Public accounts have their balance publicly accessible

+ + {purpose !== "create" || !userIsAdmin ? undefined : ( +
+
+ + + Is this account a payment provider? + + + +
+
+ )} {children} @@ -715,13 +722,14 @@ export function AccountForm({ ); } -function stringifyIbanPayto(s: PaytoString | undefined): string | undefined { +function getAccountId(type: "iban" | "x-taler-bank", s: PaytoString | undefined): string | undefined { if (s === undefined) return undefined; const p = parsePaytoUri(s); if (p === undefined) return undefined; - if (!p.isKnown) return undefined; - if (p.targetType !== "iban") return undefined; - return p.iban; + if (!p.isKnown) return ""; + if (type === "iban" && p.targetType === "iban") return p.iban; + if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account; + return ""; } { @@ -762,126 +770,128 @@ function stringifyIbanPayto(s: PaytoString | undefined): string | undefined { */ } -function PaytoField({ - name, - label, - help, - type, - value, - disabled, - onChange, - error, -}: { - error: TranslatedString | undefined; - name: string; - label: TranslatedString; - help: TranslatedString; - onChange: (s: string) => void; - type: "iban" | "x-taler-bank" | "bitcoin"; - disabled?: boolean; - value: string | undefined; -}): VNode { - if (type === "iban") { - return ( -
- -
-
- { - onChange(e.currentTarget.value); - }} - /> - value ?? ""} - /> -
- -
-

{help}

-
- ); - } - if (type === "x-taler-bank") { - return ( -
- -
-
- - value ?? ""} - /> -
- -
-

- {/* internal account id */} - {help} -

-
- ); - } - if (type === "bitcoin") { - return ( -
- -
-
- - value ?? ""} - /> - -
-
-

- {/* bitcoin address */} - {help} -

-
- ); - } - assertUnreachable(type); -} +// function PaytoField({ +// name, +// label, +// help, +// type, +// value, +// disabled, +// onChange, +// error, +// }: { +// error: TranslatedString | undefined; +// name: string; +// label: TranslatedString; +// help: TranslatedString; +// onChange: (s: string) => void; +// type: "iban" | "x-taler-bank" | "bitcoin"; +// disabled?: boolean; +// value: string | undefined; +// }): VNode { +// if (type === "iban") { +// return ( +//
+// +//
+//
+// { +// onChange(e.currentTarget.value); +// }} +// /> +// value ?? ""} +// /> +//
+// +//
+//

{help}

+//
+// ); +// } +// if (type === "x-taler-bank") { +// return ( +//
+// +//
+//
+// { +// onChange(e.currentTarget.value); +// }} +// /> +// value ?? ""} +// /> +//
+// +//
+//

+// {help} +//

+//
+// ); +// } +// if (type === "bitcoin") { +// return ( +//
+// +//
+//
+// +// value ?? ""} +// /> +// +//
+//
+//

+// {/* bitcoin address */} +// {help} +//

+//
+// ); +// } +// assertUnreachable(type); +// } diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index 41d54c43d..5528b5226 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -62,6 +62,7 @@ export function AccountList({ } } + const { accounts } = result.data.body; return ( @@ -170,15 +171,20 @@ export function AccountList({ Change password
- - Cashouts - -
+ {config.allow_conversion ? + + + + Cashouts + +
+
+ : undefined} {noBalance ? ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index c4e4266f9..23d5a1e90 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -29,7 +29,6 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { mutate } from "swr"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { RouteDefinition } from "../../route.js"; @@ -70,7 +69,6 @@ export function CreateNewAccount({ const resp = await api.createAccount(token, submitAccount); if (resp.type === "ok") { - mutate(() => true); // clean account list notifyInfo( i18n.str`Account created with password "${submitAccount.password}". The user must change the password on the next login.`, ); diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 8ec34276f..6d538575b 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -140,8 +140,10 @@ export function CreateCashout({ return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: @@ -188,8 +190,7 @@ export function CreateCashout({ * depending on the isDebit flag */ const inputAmount = Amounts.parseOrThrow( - `${form.isDebit ? regional_currency : fiat_currency}:${ - !form.amount ? "0" : form.amount + `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount }`, ); @@ -291,7 +292,7 @@ export function CreateCashout({ case HttpStatusCode.NotImplemented: return notify({ type: "error", - title: i18n.str`Cashouts are not supported`, + title: i18n.str`Cashout are disabled`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); @@ -471,9 +472,9 @@ export function CreateCashout({ cashoutDisabled ? undefined : (value) => { - form.amount = value; - updateForm(structuredClone(form)); - } + form.amount = value; + updateForm(structuredClone(form)); + } } /> {Amounts.isZero(sellFee) || - Amounts.isZero(calc.beforeFee) ? undefined : ( + Amounts.isZero(calc.beforeFee) ? undefined : (
@@ -547,7 +548,7 @@ export function CreateCashout({ {/* channel, not shown if new cashout api */} {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels - .length === 0 ? ( + .length === 0 ? (
{ if (!resultAccount.body.contact_data?.phone) return; diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index 7b251d3ca..1e70886ad 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -69,8 +69,10 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); default: assertUnreachable(result); @@ -87,7 +89,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { switch (info.case) { case HttpStatusCode.NotImplemented: { return ( - + + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: -- cgit v1.2.3