From 0388d31d364139d0a3999126b06d8ac850117ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 19 Sep 2023 00:39:00 -0300 Subject: account page --- packages/demobank-ui/src/pages/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/demobank-ui/src/pages/HomePage.tsx') diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 93a9bdfae..86e511284 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -32,7 +32,7 @@ import { useBackendContext } from "../context/backend.js"; import { getInitialBackendBaseURL } from "../hooks/backend.js"; import { notifyError, notifyInfo } from "../hooks/notification.js"; import { useSettings } from "../hooks/settings.js"; -import { AccountPage } from "./AccountPage.js"; +import { AccountPage } from "./AccountPage/index.js"; import { AdminPage } from "./AdminPage.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; -- cgit v1.2.3 From e39d5c488e2e425bd7febf694caadc17d5126401 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 20 Sep 2023 15:18:36 -0300 Subject: more ui --- packages/demobank-ui/src/assets/logo-2021.svg | 9 + packages/demobank-ui/src/components/QR.tsx | 5 +- .../src/components/Transactions/views.tsx | 14 +- packages/demobank-ui/src/forms/simplest.ts | 66 + packages/demobank-ui/src/hooks/notification.ts | 54 - .../demobank-ui/src/pages/AccountPage/index.ts | 4 +- .../demobank-ui/src/pages/AccountPage/state.ts | 10 +- packages/demobank-ui/src/pages/AdminPage.tsx | 150 +- packages/demobank-ui/src/pages/BankFrame.tsx | 409 +-- packages/demobank-ui/src/pages/BusinessAccount.tsx | 106 +- packages/demobank-ui/src/pages/HomePage.tsx | 40 +- packages/demobank-ui/src/pages/LoginForm.tsx | 335 +-- packages/demobank-ui/src/pages/PaymentOptions.tsx | 48 +- .../src/pages/PaytoWireTransferForm.tsx | 221 +- packages/demobank-ui/src/pages/QrCodeSection.tsx | 126 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 311 ++- .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 319 ++- .../src/pages/WithdrawalConfirmationQuestion.tsx | 383 ++- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 23 +- packages/demobank-ui/src/pages/rnd.ts | 2890 ++++++++++++++++++++ packages/demobank-ui/src/utils.ts | 18 +- pnpm-lock.yaml | 10 +- 22 files changed, 4451 insertions(+), 1100 deletions(-) create mode 100644 packages/demobank-ui/src/assets/logo-2021.svg create mode 100644 packages/demobank-ui/src/forms/simplest.ts delete mode 100644 packages/demobank-ui/src/hooks/notification.ts create mode 100644 packages/demobank-ui/src/pages/rnd.ts (limited to 'packages/demobank-ui/src/pages/HomePage.tsx') diff --git a/packages/demobank-ui/src/assets/logo-2021.svg b/packages/demobank-ui/src/assets/logo-2021.svg new file mode 100644 index 000000000..8c5ff3e5b --- /dev/null +++ b/packages/demobank-ui/src/assets/logo-2021.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/demobank-ui/src/components/QR.tsx index c1c159ef8..945a08867 100644 --- a/packages/demobank-ui/src/components/QR.tsx +++ b/packages/demobank-ui/src/components/QR.tsx @@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode { return (
Latest transactions
-
+
- - - - + + + + {transactions.map((item, idx) => { return ( - - + ); })} diff --git a/packages/demobank-ui/src/forms/simplest.ts b/packages/demobank-ui/src/forms/simplest.ts new file mode 100644 index 000000000..54b6b1c65 --- /dev/null +++ b/packages/demobank-ui/src/forms/simplest.ts @@ -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): FormState { + return { + when: { + disabled: true, + }, + threshold: { + // disabled: v.state === AmlExchangeBackend.AmlState.frozen, + }, + }; +} + + diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts deleted file mode 100644 index 9bf621b41..000000000 --- a/packages/demobank-ui/src/hooks/notification.ts +++ /dev/null @@ -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(); -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, -] { - const [value, setter] = useState(); - useEffect(() => { - return storage.onUpdate(NOTIFICATION_KEY, () => { - setter(storage.get(NOTIFICATION_KEY)); - }); - }); - return [value, setter]; -} diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 28fb7cb0c..ed6945f84 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -15,7 +15,7 @@ */ 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 { useComponentState } from "./state.js"; import { ReadyView, InvalidIbanView} from "./views.js"; @@ -51,7 +51,7 @@ export namespace State { status: "ready"; error: undefined; account: string, - payto: PaytoUriIBAN, + payto: PaytoUriIBAN | PaytoUriTalerBank, balance: AmountJson, balanceIsDebit: boolean, limit: AmountJson, diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index bc59c9374..2249b743e 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -15,10 +15,9 @@ */ 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 { useAccountDetails } from "../../hooks/access.js"; -import { notifyError } from "../../hooks/notification.js"; import { Props, State } from "./index.js"; 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 backend.logOut(); if (result.status === HttpStatusCode.NotFound) { - notifyError({ - title: i18n.str`Username or account label "${account}" not found`, - }); + notifyError(i18n.str`Username or account label "${account}" not found`, undefined); return { status: "error-user-not-found", error: result, @@ -62,7 +59,8 @@ export function useComponentState({ account, onLoadNotOk }: Props): State { const debitThreshold = Amounts.parseOrThrow(data.debitThreshold); 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 { status: "invalid-iban", error: result diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index 73a4f9ca3..18462bdc3 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -14,11 +14,14 @@ GNU Taler; see the file COPYING. If not, see */ -import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; import { ErrorType, HttpResponsePaginated, RequestError, + notify, + notifyError, + notifyInfo, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -39,12 +42,10 @@ import { validateIBAN, WithIntermediate, } from "../utils.js"; -import { ErrorBannerFloat } from "./BankFrame.js"; import { ShowCashoutDetails } from "./BusinessAccount.js"; import { handleNotOkResult } from "./HomePage.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { ErrorMessage, notifyInfo } from "../hooks/notification.js"; const charset = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -362,6 +363,7 @@ function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { onSuccess={() => { notifyInfo(i18n.str`Wire transfer created!`); }} + onCancel={undefined} /> ); @@ -414,7 +416,6 @@ export function UpdateAccountPassword({ const { changePassword } = useAdminAccountAPI(); const [password, setPassword] = useState(); const [repeat, setRepeat] = useState(); - const [error, saveError] = useState(); if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { @@ -431,8 +432,8 @@ export function UpdateAccountPassword({ repeat: !repeat ? i18n.str`required` : password !== repeat - ? i18n.str`password doesn't match` - : undefined, + ? i18n.str`password doesn't match` + : undefined, }); return ( @@ -442,9 +443,6 @@ export function UpdateAccountPassword({ Update password for {account} - {error && ( - saveError(undefined)} /> - )}
@@ -507,15 +505,11 @@ export function UpdateAccountPassword({ onUpdateSuccess(); } catch (error) { if (error instanceof RequestError) { - saveError(buildRequestErrorMessage(i18n, error.cause)); + notify(buildRequestErrorMessage(i18n, error.cause)); } else { - saveError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); + notifyError(i18n.str`Operation failed, please report`, (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString) } } }} @@ -540,7 +534,6 @@ function CreateNewAccount({ const [submitAccount, setSubmitAccount] = useState< SandboxBackend.Circuit.CircuitAccountData | undefined >(); - const [error, saveError] = useState(); return (
@@ -548,9 +541,6 @@ function CreateNewAccount({ New account
- {error && ( - saveError(undefined)} /> - )}
status === HttpStatusCode.Forbidden ? i18n.str`The rights to perform the operation are not sufficient` : status === HttpStatusCode.BadRequest - ? i18n.str`Input data was invalid` - : status === HttpStatusCode.Conflict - ? i18n.str`At least one registration detail was not available` - : undefined, + ? i18n.str`Input data was invalid` + : status === HttpStatusCode.Conflict + ? i18n.str`At least one registration detail was not available` + : undefined, }), ); } else { - saveError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) } } }} @@ -654,7 +643,6 @@ export function ShowAccountDetails({ const [submitAccount, setSubmitAccount] = useState< SandboxBackend.Circuit.CircuitAccountData | undefined >(); - const [error, saveError] = useState(); if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { @@ -673,9 +661,6 @@ export function ShowAccountDetails({ Business account details
- {error && ( - saveError(undefined)} /> - )}
status === HttpStatusCode.Forbidden ? i18n.str`The rights to change the account are not sufficient` : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : undefined, + ? i18n.str`The username was not found` + : undefined, }), ); } else { - saveError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) } } } @@ -788,7 +772,6 @@ function RemoveAccount({ const { i18n } = useTranslationContext(); const result = useAccountDetails(account); const { deleteAccount } = useAdminAccountAPI(); - const [error, saveError] = useState(); if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { @@ -812,7 +795,8 @@ function RemoveAccount({ Remove account: {account}
- {!isBalanceEmpty && ( + {/* {FXME: SHOW WARNING} */} + {/* {!isBalanceEmpty && ( saveError(undefined)} /> - )} - {error && ( - saveError(undefined)} /> - )} + )} */}

@@ -852,26 +833,23 @@ function RemoveAccount({ onUpdateSuccess(); } catch (error) { if (error instanceof RequestError) { - saveError( + notify( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.Forbidden ? i18n.str`The administrator specified a institutional username` : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`Balance was not zero` - : undefined, + ? i18n.str`The username was not found` + : status === HttpStatusCode.PreconditionFailed + ? i18n.str`Balance was not zero` + : undefined, }), ); } else { - saveError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error + notifyError(i18n.str`Operation failed, please report`, + (error instanceof Error ? error.message - : JSON.stringify(error), - }); + : JSON.stringify(error)) as TranslatedString); } } }} @@ -915,31 +893,31 @@ function AccountForm({ cashout_address: !newForm.cashout_address ? i18n.str`required` : !parsed - ? i18n.str`does not follow the pattern` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`only "IBAN" target are supported` - : !IBAN_REGEX.test(parsed.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(parsed.iban, i18n), + ? i18n.str`does not follow the pattern` + : !parsed.isKnown || parsed.targetType !== "iban" + ? i18n.str`only "IBAN" target are supported` + : !IBAN_REGEX.test(parsed.iban) + ? i18n.str`IBAN should have just uppercased letters and numbers` + : validateIBAN(parsed.iban, i18n), contact_data: undefinedIfEmpty({ email: !newForm.contact_data?.email ? i18n.str`required` : !EMAIL_REGEX.test(newForm.contact_data.email) - ? i18n.str`it should be an email` - : undefined, + ? i18n.str`it should be an email` + : undefined, phone: !newForm.contact_data?.phone ? i18n.str`required` : !newForm.contact_data.phone.startsWith("+") - ? i18n.str`should start with +` - : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) - ? i18n.str`phone number can't have other than numbers` - : undefined, + ? i18n.str`should start with +` + : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) + ? i18n.str`phone number can't have other than numbers` + : undefined, }), iban: !newForm.iban ? undefined //optional field : !IBAN_REGEX.test(newForm.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(newForm.iban, i18n), + ? i18n.str`IBAN should have just uppercased letters and numbers` + : validateIBAN(newForm.iban, i18n), name: !newForm.name ? i18n.str`required` : undefined, username: !newForm.username ? i18n.str`required` : undefined, }); diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 5b6d95ade..d234845a0 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,17 +15,16 @@ */ 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 { StateUpdater, useEffect, useState } from "preact/hooks"; -import talerLogo from "../assets/logo-white.svg"; import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; import { useBackendContext } from "../context/backend.js"; import { useBusinessAccountDetails } from "../hooks/circuit.js"; import { bankUiSettings } from "../settings.js"; import { useSettings } from "../hooks/settings.js"; -import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js"; import { CopyButton, CopyIcon } from "../components/CopyButton.js"; +import logo from "../assets/logo-2021.svg"; const IS_PUBLIC_ACCOUNT_ENABLED = false; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; @@ -81,16 +80,23 @@ export function BankFrame({ , ); - return (
+ return (
-
-
+
- {/* */} + {children}
@@ -207,15 +282,15 @@ export function BankFrame({ // /> // ) : undefined} - // + // // { - // backend.logOut(); - // updateSettings("currentWithdrawalOperationId", undefined); - // }} + // onClick={() => { + // backend.logOut(); + // updateSettings("currentWithdrawalOperationId", undefined); + // }} // >{i18n.str`Logout`} // // ) : undefined} @@ -225,149 +300,110 @@ export function BankFrame({ // // {children} // - // // ); } -function maybeDemoContent(content: VNode): VNode { - if (bankUiSettings.showDemoNav) { - return content; - } - return ; -} +// function maybeDemoContent(content: VNode): VNode { +// if (bankUiSettings.showDemoNav) { +// return content; +// } +// return ; +// } -export function ErrorBannerFloat({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( -
- -
- ); -} +// export function ErrorBannerFloat({ +// error, +// onClear, +// }: { +// error: ErrorMessage; +// onClear?: () => void; +// }): VNode { +// return ( +//
+// +//
+// ); +// } -function ErrorBanner({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( -
-
-

- {error.title} -

-
- {onClear && ( - { - e.preventDefault(); - onClear(); - }} - /> - )} -
-
-

{error.description}

-
- ); -} - -function StatusBanner(): VNode | null { - const [info, setInfo] = useState(); - const [error, setError] = useState(); - useEffect(() => { - return onNotificationUpdate((newValue) => { - if (newValue === undefined) { - setInfo(undefined); - setError(undefined); - } else { - if (newValue.type === "error") { - setError(newValue.error); - } else { - setInfo(newValue.info); - } - } - }); - }, []); - return ( -
- {!info ? undefined : ( -
-
-

- {info} -

-
- { - setInfo(undefined); - }} - /> +function StatusBanner(): VNode { + const notifs = useNotifications() + return
{ + notifs.map(n => { + const info = n.message.type === "info" ? n.message : undefined + const error = n.message.type === "error" ? n.message : undefined + switch (n.message.type) { + case "error": + return
+
+
+ +
+
+

{n.message.title}

+

+ +

+
+ {n.message.description && +
+ {n.message.description} +
+ }
-
- )} - {!error ? undefined : ( - { - setError(undefined); - }} - /> - )} -
- ); + case "info": + return
+
+
+ +
+
+

{n.message.title}

+ +

+ +

+
+ +
+
+ } + })} +
+ } function TestingTag(): VNode { @@ -392,12 +428,12 @@ function TestingTag(): VNode { function Footer() { return ( -
+

You can learn more about GNU Taler on our{" "} - main website. + main website.

@@ -418,4 +454,9 @@ function WelcomeAccount(): VNode { Welcome, {account} ({payto.iban})! stringifyPaytoUri(payto)} /> -} \ No newline at end of file +} + +function AccountBalance(): VNode { + + return
KUDOS 100.00
+} diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 2faf83a1c..ec71ceca6 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -17,17 +17,21 @@ import { AmountJson, Amounts, HttpStatusCode, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { HttpResponse, HttpResponsePaginated, RequestError, + notify, + notifyError, + notifyInfo, useTranslationContext, } from "@gnu-taler/web-util/browser"; 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 { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useBackendContext } from "../context/backend.js"; import { useAccountDetails } from "../hooks/access.js"; import { @@ -42,12 +46,9 @@ import { undefinedIfEmpty, } from "../utils.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 { ErrorMessage, notifyInfo } from "../hooks/notification.js"; -import { Amount } from "./WalletWithdrawForm.js"; +import { LoginForm } from "./LoginForm.js"; +import { Amount } from "./PaytoWireTransferForm.js"; interface Props { onClose: () => void; @@ -225,7 +226,6 @@ function CreateCashout({ const { i18n } = useTranslationContext(); const ratiosResult = useRatiosAndFeeConfig(); const result = useAccountDetails(account); - const [error, saveError] = useState(); const { estimateByCredit: calculateFromCredit, estimateByDebit: calculateFromDebit, @@ -268,15 +268,15 @@ function CreateCashout({ calculateFromDebit(amount, sellFee, sellRate) .then((r) => { setCalc(r); - saveError(undefined); }) .catch((error) => { - saveError( + notify( error instanceof RequestError ? buildRequestErrorMessage(i18n, error.cause) : { + type: "error", 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) .then((r) => { setCalc(r); - saveError(undefined); }) .catch((error) => { - saveError( + notify( error instanceof RequestError ? buildRequestErrorMessage(i18n, error.cause) : { + type: "error", title: i18n.str`Could not estimate the cashout`, description: error.message, }, @@ -321,9 +321,6 @@ function CreateCashout({ return (
- {error && ( - saveError(undefined)} /> - )}

New cashout

@@ -341,13 +338,15 @@ function CreateCashout({ />
-
- +
-
- + @@ -401,16 +403,18 @@ function CreateCashout({ {Amounts.isZero(sellFee) ? undefined : (
- +
- + @@ -418,10 +422,11 @@ function CreateCashout({ )}
- @@ -511,7 +516,7 @@ function CreateCashout({ onComplete(res.data.uuid); } catch (error) { if (error instanceof RequestError) { - saveError( + notify( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.BadRequest @@ -530,14 +535,13 @@ function CreateCashout({ }), ); } else { - saveError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); - } + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } } }} > @@ -565,7 +569,6 @@ export function ShowCashoutDetails({ const result = useCashoutDetails(id); const { abortCashout, confirmCashout } = useCircuitAccountAPI(); const [code, setCode] = useState(undefined); - const [error, saveError] = useState(); if (!result.ok) return onLoadNotOk(result); const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, @@ -574,9 +577,6 @@ export function ShowCashoutDetails({ return (

Cashout details {id}

- {error && ( - saveError(undefined)} /> - )}
+ + ); } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index cf3f41deb..c82c1b28d 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -15,10 +15,9 @@ */ 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 { useState } from "preact/hooks"; -import { notifyInfo } from "../hooks/notification.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; import { useSettings } from "../hooks/settings.js"; @@ -31,7 +30,8 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { const { i18n } = useTranslationContext(); 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 (
@@ -41,34 +41,32 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
{/* */} - {/* */}
) - {/* return ( -
-
-
- - -
-
-
- ); */} } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 1107360bd..5e0624cbf 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -19,17 +19,21 @@ import { Amounts, HttpStatusCode, Logger, - parsePaytoUri + TranslatedString, + buildPayto, + parsePaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { RequestError, + notify, + notifyError, useTranslationContext, } 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 { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAccessAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { buildRequestErrorMessage, undefinedIfEmpty, @@ -41,10 +45,12 @@ const logger = new Logger("PaytoWireTransferForm"); export function PaytoWireTransferForm({ focus, onSuccess, + onCancel, limit, }: { focus?: boolean; onSuccess: () => void; + onCancel: (() => void) | undefined; limit: AmountJson; }): VNode { const [isRawPayto, setIsRawPayto] = useState(false); @@ -105,7 +111,51 @@ export function PaytoWireTransferForm({ ? i18n.str`IBAN should have just uppercased letters and numbers` : 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 (

Transfer details

@@ -118,7 +168,7 @@ export function PaytoWireTransferForm({ }} /> - + form @@ -133,7 +183,7 @@ export function PaytoWireTransferForm({ }} /> - + payto:// @@ -143,23 +193,31 @@ export function PaytoWireTransferForm({
-
+ { + e.preventDefault() + }} + >
{!isRawPayto ? -
- +
+
{ @@ -171,21 +229,18 @@ export function PaytoWireTransferForm({ isDirty={iban !== undefined} />
-

the receiver of the money

-
- -
+

the receiver of the money

-
- +
+
-
-

some text to identify the transfer

- -
- -
+

some text to identify the transfer

-
- -
- -
+
+ + { + setAmount(d) + }} + /> + +

amount to transfer

-
-
:
- +
{ rawPaytoInputSetter(e.currentTarget.value); }} @@ -244,9 +302,23 @@ export function PaytoWireTransferForm({ }
-
- - + :
+ } +
@@ -262,8 +334,6 @@ export function PaytoWireTransferForm({ // onSubmit={(e) => { // e.preventDefault(); // }} - // autoCapitalize="none" - // autoCorrect="off" // > //   @@ -318,39 +388,7 @@ export function PaytoWireTransferForm({ // if (!(iban && subject && amount)) { // 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), - // }); - // } - // } + // }} // /> // // ); } +export function Amount( + { + currency, + name, + value, + error, + onChange, + }: { + error?: string; + currency: string; + name: string; + value: string | undefined; + onChange?: (s: string) => void; + }, + ref: Ref, +): VNode { + return ( +
+
+
+ {currency} +
+ { + if (onChange) { + onChange(e.currentTarget.value); + } + }} + /> +
+ +
+ ); +} diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index c27984569..7c1b3bdc5 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -17,17 +17,19 @@ import { HttpStatusCode, stringifyWithdrawUri, + TranslatedString, WithdrawUriResult, } from "@gnu-taler/taler-util"; import { + notify, + notifyError, RequestError, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; import { useAccessAnonAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { buildRequestErrorMessage } from "../utils.js"; export function QrCodeSection({ @@ -49,47 +51,87 @@ export function QrCodeSection({ const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); 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 ( -
-

{i18n.str`Charge your GNU Taler wallet`}

-
-
- - Continue with GNU Taler - -

{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}

- - { - e.preventDefault(); - try { - await abortWithdrawal(withdrawUri.withdrawalOperationId); - onAborted(); - } catch (error) { - if (error instanceof RequestError) { - notifyError( - 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({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); - } - } - }} - >{i18n.str`Cancel`} + +
+
+

+ If you have a Taler wallet installed in this device +

+ +
+

+ You will see the details of the operation in your wallet including the fees (if applies). + If you still one you can install it from here. +

+
+
+
+ +
+
+
+ +
+
+

+ Or if you have the wallet in another device +

+
+ Scan the QR below to start the withdrawal +
+
+ +
+
+
+
-
-
+
+ +
); } diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index e52a5b11b..b912b9060 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -13,19 +13,21 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ -import { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; +import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util"; import { RequestError, + notify, + notifyError, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; import { useTestingAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { bankUiSettings } from "../settings.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { getRandomPassword, getRandomUsername } from "./rnd.js"; const logger = new Logger("RegistrationPage"); @@ -61,147 +63,214 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode { username: !username ? i18n.str`Missing username` : !USERNAME_REGEX.test(username) - ? i18n.str`Use letters and numbers only, and start with a lowercase letter` - : undefined, + ? i18n.str`Use letters and numbers only, and start with a lowercase letter` + : undefined, password: !password ? i18n.str`Missing password` : undefined, repeatPassword: !repeatPassword ? i18n.str`Missing password` : repeatPassword !== password - ? i18n.str`Passwords don't match` - : undefined, + ? i18n.str`Passwords don't match` + : 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 { + 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 ( -

{i18n.str`Welcome to ${bankUiSettings.bankName}!`}

-
-
- + +
+
+

{i18n.str`Sign up!`}

+
+ +
+ { e.preventDefault(); }} autoCapitalize="none" autoCorrect="off" > -
-

{i18n.str`Please register!`}

-

- -

- { - setUsername(e.currentTarget.value); - }} - /> - -

- -

- { - setPassword(e.currentTarget.value); - }} - /> - -

- -

- { - setRepeatPassword(e.currentTarget.value); - }} - /> - -
-
+ +
+
+ +
+
+ { + setPassword(e.currentTarget.value); + }} + /> + +
+
+ +
+
+ +
+
+ { + setRepeatPassword(e.currentTarget.value); + }} + /> + +
+
+ +
+ - {/* FIXME: should use a different color */} -
+ + +

+ +

-
+
+ ); } diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index da624f61b..6574ec934 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -19,19 +19,21 @@ import { Amounts, HttpStatusCode, Logger, + TranslatedString, parseWithdrawUri, } from "@gnu-taler/taler-util"; import { RequestError, + notify, + notifyError, useTranslationContext, } 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 { useAccessAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { forwardRef } from "preact/compat"; +import { Amount } from "./PaytoWireTransferForm.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); @@ -40,10 +42,12 @@ export function WalletWithdrawForm({ focus, limit, onSuccess, + onCancel, }: { limit: AmountJson; focus?: boolean; onSuccess: (operationId: string) => void; + onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); const { createWithdrawal } = useAccessAPI(); @@ -71,136 +75,195 @@ export function WalletWithdrawForm({ : 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 (
+
+

Prepare your wallet

+

+ Upon starting you will receive the money in your digital wallet, if you don't have one please install one from here. +

+

+ After using your wallet you will be redirected here to confirm or cancel the operation. +

+
{ - e.preventDefault(); - }} + 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() + }} > -

- -   - { - setAmountStr(v); - }} - error={errors?.amount} - ref={ref} - /> -

-

-

- { - e.preventDefault(); - if (!parsedAmount) return; - try { - const result = await createWithdrawal({ - amount: Amounts.stringify(parsedAmount), - }); - const uri = parseWithdrawUri(result.data.taler_withdraw_uri); - if (!uri) { - return notifyError({ - title: i18n.str`Server responded with an invalid withdraw URI`, - description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`, - }); - } else { - onSuccess(uri.withdrawalOperationId); - } - } catch (error) { - if (error instanceof RequestError) { - notifyError( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The operation was rejected due to insufficient funds` - : undefined, - }), - ); - } else { - notifyError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); - } - } - }} - /> -
-

- - ); -} +
+
+
+ + { + setAmountStr(v); + }} + error={errors?.amount} + ref={ref} + /> +
+
+ + + + + + +
+ +
+
+
+ +
- -
+ + +
); } + +// export function Amount( +// { +// currency, +// value, +// error, +// onChange, +// }: { +// error?: string; +// currency: string; +// value: string | undefined; +// onChange?: (s: string) => void; +// }, +// ref: Ref, +// ): VNode { +// return ( +//
+//
+// +// { +// if (onChange) { +// onChange(e.currentTarget.value); +// } +// }} +// /> +//
+// +//
+// ); +// } diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 2fa8e51b5..28f00169d 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -15,26 +15,40 @@ */ import { + AmountJson, + Amounts, HttpStatusCode, Logger, + PaytoUri, + PaytoUriGeneric, + PaytoUriIBAN, + PaytoUriTalerBank, + TranslatedString, WithdrawUriResult, } from "@gnu-taler/taler-util"; import { RequestError, + notify, + notifyError, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; import { useAccessAnonAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { Amount } from "./PaytoWireTransferForm.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); interface Props { onAborted: () => void; withdrawUri: WithdrawUriResult; + details: { + account: PaytoUri, + reserve: string, + amount: AmountJson, + } } /** * Additional authentication required to complete the operation. @@ -42,6 +56,7 @@ interface Props { */ export function WithdrawalConfirmationQuestion({ onAborted, + details, withdrawUri, }: Props): VNode { const { i18n } = useTranslationContext(); @@ -60,135 +75,257 @@ export function WithdrawalConfirmationQuestion({ answer: !captchaAnswer ? i18n.str`Answer the question before continue` : Number.isNaN(answer) - ? i18n.str`The answer should be a number` - : answer !== captchaNumbers.a + captchaNumbers.b - ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` - : undefined, + ? i18n.str`The answer should be a number` + : answer !== captchaNumbers.a + captchaNumbers.b + ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` + : 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 ( -

{i18n.str`Confirm Withdrawal`}

-
-
-
{ - e.preventDefault(); - }} - autoCapitalize="none" - autoCorrect="off" - > -
-

{i18n.str`Authorize withdrawal by solving challenge`}

-

- -   - { - setCaptchaAnswer(e.currentTarget.value); - }} - /> - -

-

- -   - -

+ })()} +
+
Withdrawal identification
+
{details.reserve}
+
+
+
Amount
+
{Amounts.stringifyValue(details.amount)}
+
+ +
+
+ +
+
+ + + + + + + +
+
+
+ +
+
+

Answer the next question to authorize the wire transfer

+
+ { + e.preventDefault() + }} + > +
+ +
+
+ { + // if (onChange) { + // onChange(e.currentTarget.value); + // } + // }} + /> +
+ +
+
+
+ + +
+ +
- -
-

- - A this point, a real bank would ask for an additional - authentication proof (PIN/TAN, one time password, ..), instead - of a simple calculation. - -

- +
+ ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 80fdac3c8..3b983c2d4 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -15,15 +15,16 @@ */ import { + Amounts, HttpStatusCode, Logger, WithdrawUriResult, + parsePaytoUri, } 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 { Loading } from "../components/Loading.js"; import { useWithdrawalDetails } from "../hooks/access.js"; -import { notifyInfo } from "../hooks/notification.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; import { QrCodeSection } from "./QrCodeSection.js"; @@ -127,6 +128,19 @@ export function WithdrawalQRCode({ } + if (!data.selected_reserve_pub) { + return
+ the exchange is selcted but no reserve pub +
+ } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return
+ the exchange is selcted but no account +
+ } if (!data.selection_done) { return ( @@ -144,6 +158,11 @@ export function WithdrawalQRCode({ return ( { notifyInfo(i18n.str`Operation canceled`); clearCurrentWithdrawal() diff --git a/packages/demobank-ui/src/pages/rnd.ts b/packages/demobank-ui/src/pages/rnd.ts new file mode 100644 index 000000000..8c9bae875 --- /dev/null +++ b/packages/demobank-ui/src/pages/rnd.ts @@ -0,0 +1,2890 @@ +import { createEddsaKeyPair, encodeCrock, getRandomBytes } from "@gnu-taler/taler-util" + + +const noun = [ + "people", + "history", + "way", + "art", + "world", + "information", + "map", + "two", + "family", + "government", + "health", + "system", + "computer", + "meat", + "year", + "thanks", + "music", + "person", + "reading", + "method", + "data", + "food", + "understanding", + "theory", + "law", + "bird", + "literature", + "problem", + "software", + "control", + "knowledge", + "power", + "ability", + "economics", + "love", + "internet", + "television", + "science", + "library", + "nature", + "fact", + "product", + "idea", + "temperature", + "investment", + "area", + "society", + "activity", + "story", + "industry", + "media", + "thing", + "oven", + "community", + "definition", + "safety", + "quality", + "development", + "language", + "management", + "player", + "variety", + "video", + "week", + "security", + "country", + "exam", + "movie", + "organization", + "equipment", + "physics", + "analysis", + "policy", + "series", + "thought", + "basis", + "boyfriend", + "direction", + "strategy", + "technology", + "army", + "camera", + "freedom", + "paper", + "environment", + "child", + "instance", + "month", + "truth", + "marketing", + "university", + "writing", + "article", + "department", + "difference", + "goal", + "news", + "audience", + "fishing", + "growth", + "income", + "marriage", + "user", + "combination", + "failure", + "meaning", + "medicine", + "philosophy", + "teacher", + "communication", + "night", + "chemistry", + "disease", + "disk", + "energy", + "nation", + "road", + "role", + "soup", + "advertising", + "location", + "success", + "addition", + "apartment", + "education", + "math", + "moment", + "painting", + "politics", + "attention", + "decision", + "event", + "property", + "shopping", + "student", + "wood", + "competition", + "distribution", + "entertainment", + "office", + "population", + "president", + "unit", + "category", + "cigarette", + "context", + "introduction", + "opportunity", + "performance", + "driver", + "flight", + "length", + "magazine", + "newspaper", + "relationship", + "teaching", + "cell", + "dealer", + "finding", + "lake", + "member", + "message", + "phone", + "scene", + "appearance", + "association", + "concept", + "customer", + "death", + "discussion", + "housing", + "inflation", + "insurance", + "mood", + "woman", + "advice", + "blood", + "effort", + "expression", + "importance", + "opinion", + "payment", + "reality", + "responsibility", + "situation", + "skill", + "statement", + "wealth", + "application", + "city", + "county", + "depth", + "estate", + "foundation", + "grandmother", + "heart", + "perspective", + "photo", + "recipe", + "studio", + "topic", + "collection", + "depression", + "imagination", + "passion", + "percentage", + "resource", + "setting", + "ad", + "agency", + "college", + "connection", + "criticism", + "debt", + "description", + "memory", + "patience", + "secretary", + "solution", + "administration", + "aspect", + "attitude", + "director", + "personality", + "psychology", + "recommendation", + "response", + "selection", + "storage", + "version", + "alcohol", + "argument", + "complaint", + "contract", + "emphasis", + "highway", + "loss", + "membership", + "possession", + "preparation", + "steak", + "union", + "agreement", + "cancer", + "currency", + "employment", + "engineering", + "entry", + "interaction", + "mixture", + "preference", + "region", + "republic", + "tradition", + "virus", + "actor", + "classroom", + "delivery", + "device", + "difficulty", + "drama", + "election", + "engine", + "football", + "guidance", + "hotel", + "owner", + "priority", + "protection", + "suggestion", + "tension", + "variation", + "anxiety", + "atmosphere", + "awareness", + "bath", + "bread", + "candidate", + "climate", + "comparison", + "confusion", + "construction", + "elevator", + "emotion", + "employee", + "employer", + "guest", + "height", + "leadership", + "mall", + "manager", + "operation", + "recording", + "sample", + "transportation", + "charity", + "cousin", + "disaster", + "editor", + "efficiency", + "excitement", + "extent", + "feedback", + "guitar", + "homework", + "leader", + "mom", + "outcome", + "permission", + "presentation", + "promotion", + "reflection", + "refrigerator", + "resolution", + "revenue", + "session", + "singer", + "tennis", + "basket", + "bonus", + "cabinet", + "childhood", + "church", + "clothes", + "coffee", + "dinner", + "drawing", + "hair", + "hearing", + "initiative", + "judgment", + "lab", + "measurement", + "mode", + "mud", + "orange", + "poetry", + "police", + "possibility", + "procedure", + "queen", + "ratio", + "relation", + "restaurant", + "satisfaction", + "sector", + "signature", + "significance", + "song", + "tooth", + "town", + "vehicle", + "volume", + "wife", + "accident", + "airport", + "appointment", + "arrival", + "assumption", + "baseball", + "chapter", + "committee", + "conversation", + "database", + "enthusiasm", + "error", + "explanation", + "farmer", + "gate", + "girl", + "hall", + "historian", + "hospital", + "injury", + "instruction", + "maintenance", + "manufacturer", + "meal", + "perception", + "pie", + "poem", + "presence", + "proposal", + "reception", + "replacement", + "revolution", + "river", + "son", + "speech", + "tea", + "village", + "warning", + "winner", + "worker", + "writer", + "assistance", + "breath", + "buyer", + "chest", + "chocolate", + "conclusion", + "contribution", + "cookie", + "courage", + "dad", + "desk", + "drawer", + "establishment", + "examination", + "garbage", + "grocery", + "honey", + "impression", + "improvement", + "independence", + "insect", + "inspection", + "inspector", + "king", + "ladder", + "menu", + "penalty", + "piano", + "potato", + "profession", + "professor", + "quantity", + "reaction", + "requirement", + "salad", + "sister", + "supermarket", + "tongue", + "weakness", + "wedding", + "affair", + "ambition", + "analyst", + "apple", + "assignment", + "assistant", + "bathroom", + "bedroom", + "beer", + "birthday", + "celebration", + "championship", + "cheek", + "client", + "consequence", + "departure", + "diamond", + "dirt", + "ear", + "fortune", + "friendship", + "funeral", + "gene", + "girlfriend", + "hat", + "indication", + "intention", + "lady", + "midnight", + "negotiation", + "obligation", + "passenger", + "pizza", + "platform", + "poet", + "pollution", + "recognition", + "reputation", + "shirt", + "sir", + "speaker", + "stranger", + "surgery", + "sympathy", + "tale", + "throat", + "trainer", + "uncle", + "youth", + "time", + "work", + "film", + "water", + "money", + "example", + "while", + "business", + "study", + "game", + "life", + "form", + "air", + "day", + "place", + "number", + "part", + "field", + "fish", + "back", + "process", + "heat", + "hand", + "experience", + "job", + "book", + "end", + "point", + "type", + "home", + "economy", + "value", + "body", + "market", + "guide", + "interest", + "state", + "radio", + "course", + "company", + "price", + "size", + "card", + "list", + "mind", + "trade", + "line", + "care", + "group", + "risk", + "word", + "fat", + "force", + "key", + "light", + "training", + "name", + "school", + "top", + "amount", + "level", + "order", + "practice", + "research", + "sense", + "service", + "piece", + "web", + "boss", + "sport", + "fun", + "house", + "page", + "term", + "test", + "answer", + "sound", + "focus", + "matter", + "kind", + "soil", + "board", + "oil", + "picture", + "access", + "garden", + "range", + "rate", + "reason", + "future", + "site", + "demand", + "exercise", + "image", + "case", + "cause", + "coast", + "action", + "age", + "bad", + "boat", + "record", + "result", + "section", + "building", + "mouse", + "cash", + "class", + "nothing", + "period", + "plan", + "store", + "tax", + "side", + "subject", + "space", + "rule", + "stock", + "weather", + "chance", + "figure", + "man", + "model", + "source", + "beginning", + "earth", + "program", + "chicken", + "design", + "feature", + "head", + "material", + "purpose", + "question", + "rock", + "salt", + "act", + "birth", + "car", + "dog", + "object", + "scale", + "sun", + "note", + "profit", + "rent", + "speed", + "style", + "war", + "bank", + "craft", + "half", + "inside", + "outside", + "standard", + "bus", + "exchange", + "eye", + "fire", + "position", + "pressure", + "stress", + "advantage", + "benefit", + "box", + "frame", + "issue", + "step", + "cycle", + "face", + "item", + "metal", + "paint", + "review", + "room", + "screen", + "structure", + "view", + "account", + "ball", + "discipline", + "medium", + "share", + "balance", + "bit", + "black", + "bottom", + "choice", + "gift", + "impact", + "machine", + "shape", + "tool", + "wind", + "address", + "average", + "career", + "culture", + "morning", + "pot", + "sign", + "table", + "task", + "condition", + "contact", + "credit", + "egg", + "hope", + "ice", + "network", + "north", + "square", + "attempt", + "date", + "effect", + "link", + "post", + "star", + "voice", + "capital", + "challenge", + "friend", + "self", + "shot", + "brush", + "couple", + "debate", + "exit", + "front", + "function", + "lack", + "living", + "plant", + "plastic", + "spot", + "summer", + "taste", + "theme", + "track", + "wing", + "brain", + "button", + "click", + "desire", + "foot", + "gas", + "influence", + "notice", + "rain", + "wall", + "base", + "damage", + "distance", + "feeling", + "pair", + "savings", + "staff", + "sugar", + "target", + "text", + "animal", + "author", + "budget", + "discount", + "file", + "ground", + "lesson", + "minute", + "officer", + "phase", + "reference", + "register", + "sky", + "stage", + "stick", + "title", + "trouble", + "bowl", + "bridge", + "campaign", + "character", + "club", + "edge", + "evidence", + "fan", + "letter", + "lock", + "maximum", + "novel", + "option", + "pack", + "park", + "plenty", + "quarter", + "skin", + "sort", + "weight", + "baby", + "background", + "carry", + "dish", + "factor", + "fruit", + "glass", + "joint", + "master", + "muscle", + "red", + "strength", + "traffic", + "trip", + "vegetable", + "appeal", + "chart", + "gear", + "ideal", + "kitchen", + "land", + "log", + "mother", + "net", + "party", + "principle", + "relative", + "sale", + "season", + "signal", + "spirit", + "street", + "tree", + "wave", + "belt", + "bench", + "commission", + "copy", + "drop", + "minimum", + "path", + "progress", + "project", + "sea", + "south", + "status", + "stuff", + "ticket", + "tour", + "angle", + "blue", + "breakfast", + "confidence", + "daughter", + "degree", + "doctor", + "dot", + "dream", + "duty", + "essay", + "father", + "fee", + "finance", + "hour", + "juice", + "limit", + "luck", + "milk", + "mouth", + "peace", + "pipe", + "seat", + "stable", + "storm", + "substance", + "team", + "trick", + "afternoon", + "bat", + "beach", + "blank", + "catch", + "chain", + "consideration", + "cream", + "crew", + "detail", + "gold", + "interview", + "kid", + "mark", + "match", + "mission", + "pain", + "pleasure", + "score", + "screw", + "sex", + "shop", + "shower", + "suit", + "tone", + "window", + "agent", + "band", + "block", + "bone", + "calendar", + "cap", + "coat", + "contest", + "corner", + "court", + "cup", + "district", + "door", + "east", + "finger", + "garage", + "guarantee", + "hole", + "hook", + "implement", + "layer", + "lecture", + "lie", + "manner", + "meeting", + "nose", + "parking", + "partner", + "profile", + "respect", + "rice", + "routine", + "schedule", + "swimming", + "telephone", + "tip", + "winter", + "airline", + "bag", + "battle", + "bed", + "bill", + "bother", + "cake", + "code", + "curve", + "designer", + "dimension", + "dress", + "ease", + "emergency", + "evening", + "extension", + "farm", + "fight", + "gap", + "grade", + "holiday", + "horror", + "horse", + "host", + "husband", + "loan", + "mistake", + "mountain", + "nail", + "noise", + "occasion", + "package", + "patient", + "pause", + "phrase", + "proof", + "race", + "relief", + "sand", + "sentence", + "shoulder", + "smoke", + "stomach", + "string", + "tourist", + "towel", + "vacation", + "west", + "wheel", + "wine", + "arm", + "aside", + "associate", + "bet", + "blow", + "border", + "branch", + "breast", + "brother", + "buddy", + "bunch", + "chip", + "coach", + "cross", + "document", + "draft", + "dust", + "expert", + "floor", + "god", + "golf", + "habit", + "iron", + "judge", + "knife", + "landscape", + "league", + "mail", + "mess", + "native", + "opening", + "parent", + "pattern", + "pin", + "pool", + "pound", + "request", + "salary", + "shame", + "shelter", + "shoe", + "silver", + "tackle", + "tank", + "trust", + "assist", + "bake", + "bar", + "bell", + "bike", + "blame", + "boy", + "brick", + "chair", + "closet", + "clue", + "collar", + "comment", + "conference", + "devil", + "diet", + "fear", + "fuel", + "glove", + "jacket", + "lunch", + "monitor", + "mortgage", + "nurse", + "pace", + "panic", + "peak", + "plane", + "reward", + "row", + "sandwich", + "shock", + "spite", + "spray", + "surprise", + "till", + "transition", + "weekend", + "welcome", + "yard", + "alarm", + "bend", + "bicycle", + "bite", + "blind", + "bottle", + "cable", + "candle", + "clerk", + "cloud", + "concert", + "counter", + "flower", + "grandfather", + "harm", + "knee", + "lawyer", + "leather", + "load", + "mirror", + "neck", + "pension", + "plate", + "purple", + "ruin", + "ship", + "skirt", + "slice", + "snow", + "specialist", + "stroke", + "switch", + "trash", + "tune", + "zone", + "anger", + "award", + "bid", + "bitter", + "boot", + "bug", + "camp", + "candy", + "carpet", + "cat", + "champion", + "channel", + "clock", + "comfort", + "cow", + "crack", + "engineer", + "entrance", + "fault", + "grass", + "guy", + "hell", + "highlight", + "incident", + "island", + "joke", + "jury", + "leg", + "lip", + "mate", + "motor", + "nerve", + "passage", + "pen", + "pride", + "priest", + "prize", + "promise", + "resident", + "resort", + "ring", + "roof", + "rope", + "sail", + "scheme", + "script", + "sock", + "station", + "toe", + "tower", + "truck", + "witness", + "a", + "you", + "it", + "can", + "will", + "if", + "one", + "many", + "most", + "other", + "use", + "make", + "good", + "look", + "help", + "go", + "great", + "being", + "few", + "might", + "still", + "public", + "read", + "keep", + "start", + "give", + "human", + "local", + "general", + "she", + "specific", + "long", + "play", + "feel", + "high", + "tonight", + "put", + "common", + "set", + "change", + "simple", + "past", + "big", + "possible", + "particular", + "today", + "major", + "personal", + "current", + "national", + "cut", + "natural", + "physical", + "show", + "try", + "check", + "second", + "call", + "move", + "pay", + "let", + "increase", + "single", + "individual", + "turn", + "ask", + "buy", + "guard", + "hold", + "main", + "offer", + "potential", + "professional", + "international", + "travel", + "cook", + "alternative", + "following", + "special", + "working", + "whole", + "dance", + "excuse", + "cold", + "commercial", + "low", + "purchase", + "deal", + "primary", + "worth", + "fall", + "necessary", + "positive", + "produce", + "search", + "present", + "spend", + "talk", + "creative", + "tell", + "cost", + "drive", + "green", + "support", + "glad", + "remove", + "return", + "run", + "complex", + "due", + "effective", + "middle", + "regular", + "reserve", + "independent", + "leave", + "original", + "reach", + "rest", + "serve", + "watch", + "beautiful", + "charge", + "active", + "break", + "negative", + "safe", + "stay", + "visit", + "visual", + "affect", + "cover", + "report", + "rise", + "walk", + "white", + "beyond", + "junior", + "pick", + "unique", + "anything", + "classic", + "final", + "lift", + "mix", + "private", + "stop", + "teach", + "western", + "concern", + "familiar", + "fly", + "official", + "broad", + "comfortable", + "gain", + "maybe", + "rich", + "save", + "stand", + "young", + "fail", + "heavy", + "hello", + "lead", + "listen", + "valuable", + "worry", + "handle", + "leading", + "meet", + "release", + "sell", + "finish", + "normal", + "press", + "ride", + "secret", + "spread", + "spring", + "tough", + "wait", + "brown", + "deep", + "display", + "flow", + "hit", + "objective", + "shoot", + "touch", + "cancel", + "chemical", + "cry", + "dump", + "extreme", + "push", + "conflict", + "eat", + "fill", + "formal", + "jump", + "kick", + "opposite", + "pass", + "pitch", + "remote", + "total", + "treat", + "vast", + "abuse", + "beat", + "burn", + "deposit", + "print", + "raise", + "sleep", + "somewhere", + "advance", + "anywhere", + "consist", + "dark", + "double", + "draw", + "equal", + "fix", + "hire", + "internal", + "join", + "kill", + "sensitive", + "tap", + "win", + "attack", + "claim", + "constant", + "drag", + "drink", + "guess", + "minor", + "pull", + "raw", + "soft", + "solid", + "wear", + "weird", + "wonder", + "annual", + "count", + "dead", + "doubt", + "feed", + "forever", + "impress", + "nobody", + "repeat", + "round", + "sing", + "slide", + "strip", + "whereas", + "wish", + "combine", + "command", + "dig", + "divide", + "equivalent", + "hang", + "hunt", + "initial", + "march", + "mention", + "smell", + "spiritual", + "survey", + "tie", + "adult", + "brief", + "crazy", + "escape", + "gather", + "hate", + "prior", + "repair", + "rough", + "sad", + "scratch", + "sick", + "strike", + "employ", + "external", + "hurt", + "illegal", + "laugh", + "lay", + "mobile", + "nasty", + "ordinary", + "respond", + "royal", + "senior", + "split", + "strain", + "struggle", + "swim", + "train", + "upper", + "wash", + "yellow", + "convert", + "crash", + "dependent", + "fold", + "funny", + "grab", + "hide", + "miss", + "permit", + "quote", + "recover", + "resolve", + "roll", + "sink", + "slip", + "spare", + "suspect", + "sweet", + "swing", + "twist", + "upstairs", + "usual", + "abroad", + "brave", + "calm", + "concentrate", + "estimate", + "grand", + "male", + "mine", + "prompt", + "quiet", + "refuse", + "regret", + "reveal", + "rush", + "shake", + "shift", + "shine", + "steal", + "suck", + "surround", + "anybody", + "bear", + "brilliant", + "dare", + "dear", + "delay", + "drunk", + "female", + "hurry", + "inevitable", + "invite", + "kiss", + "neat", + "pop", + "punch", + "quit", + "reply", + "representative", + "resist", + "rip", + "rub", + "silly", + "smile", + "spell", + "stretch", + "stupid", + "tear", + "temporary", + "tomorrow", + "wake", + "wrap", + "yesterday" +] + +const adj = [ + "abandoned", + "able", + "absolute", + "adorable", + "adventurous", + "academic", + "acceptable", + "acclaimed", + "accomplished", + "accurate", + "aching", + "acidic", + "acrobatic", + "active", + "actual", + "adept", + "admirable", + "admired", + "adolescent", + "adorable", + "adored", + "advanced", + "afraid", + "affectionate", + "aged", + "aggravating", + "aggressive", + "agile", + "agitated", + "agonizing", + "agreeable", + "ajar", + "alarmed", + "alarming", + "alert", + "alienated", + "alive", + "all", + "altruistic", + "amazing", + "ambitious", + "ample", + "amused", + "amusing", + "anchored", + "ancient", + "angelic", + "angry", + "anguished", + "animated", + "annual", + "another", + "antique", + "anxious", + "any", + "apprehensive", + "appropriate", + "apt", + "arctic", + "arid", + "aromatic", + "artistic", + "ashamed", + "assured", + "astonishing", + "athletic", + "attached", + "attentive", + "attractive", + "austere", + "authentic", + "authorized", + "automatic", + "avaricious", + "average", + "aware", + "awesome", + "awful", + "awkward", + "babyish", + "bad", + "back", + "baggy", + "bare", + "barren", + "basic", + "beautiful", + "belated", + "beloved", + "beneficial", + "better", + "best", + "bewitched", + "big", + "big-hearted", + "biodegradable", + "bite-sized", + "bitter", + "black", + "black-and-white", + "bland", + "blank", + "blaring", + "bleak", + "blind", + "blissful", + "blond", + "blue", + "blushing", + "bogus", + "boiling", + "bold", + "bony", + "boring", + "bossy", + "both", + "bouncy", + "bountiful", + "bowed", + "brave", + "breakable", + "brief", + "bright", + "brilliant", + "brisk", + "broken", + "bronze", + "brown", + "bruised", + "bubbly", + "bulky", + "bumpy", + "buoyant", + "burdensome", + "burly", + "bustling", + "busy", + "buttery", + "buzzing", + "calculating", + "calm", + "candid", + "canine", + "capital", + "carefree", + "careful", + "careless", + "caring", + "cautious", + "cavernous", + "celebrated", + "charming", + "cheap", + "cheerful", + "cheery", + "chief", + "chilly", + "chubby", + "circular", + "classic", + "clean", + "clear", + "clear-cut", + "clever", + "close", + "closed", + "cloudy", + "clueless", + "clumsy", + "cluttered", + "coarse", + "cold", + "colorful", + "colorless", + "colossal", + "comfortable", + "common", + "compassionate", + "competent", + "complete", + "complex", + "complicated", + "composed", + "concerned", + "concrete", + "confused", + "conscious", + "considerate", + "constant", + "content", + "conventional", + "cooked", + "cool", + "cooperative", + "coordinated", + "corny", + "corrupt", + "costly", + "courageous", + "courteous", + "crafty", + "crazy", + "creamy", + "creative", + "creepy", + "criminal", + "crisp", + "critical", + "crooked", + "crowded", + "cruel", + "crushing", + "cuddly", + "cultivated", + "cultured", + "cumbersome", + "curly", + "curvy", + "cute", + "cylindrical", + "damaged", + "damp", + "dangerous", + "dapper", + "daring", + "darling", + "dark", + "dazzling", + "dead", + "deadly", + "deafening", + "dear", + "dearest", + "decent", + "decimal", + "decisive", + "deep", + "defenseless", + "defensive", + "defiant", + "deficient", + "definite", + "definitive", + "delayed", + "delectable", + "delicious", + "delightful", + "delirious", + "demanding", + "dense", + "dental", + "dependable", + "dependent", + "descriptive", + "deserted", + "detailed", + "determined", + "devoted", + "different", + "difficult", + "digital", + "diligent", + "dim", + "dimpled", + "dimwitted", + "direct", + "disastrous", + "discrete", + "disfigured", + "disgusting", + "disloyal", + "dismal", + "distant", + "downright", + "dreary", + "dirty", + "disguised", + "dishonest", + "dismal", + "distant", + "distinct", + "distorted", + "dizzy", + "dopey", + "doting", + "double", + "downright", + "drab", + "drafty", + "dramatic", + "dreary", + "droopy", + "dry", + "dual", + "dull", + "dutiful", + "each", + "eager", + "earnest", + "early", + "easy", + "easy-going", + "ecstatic", + "edible", + "educated", + "elaborate", + "elastic", + "elated", + "elderly", + "electric", + "elegant", + "elementary", + "elliptical", + "embarrassed", + "embellished", + "eminent", + "emotional", + "empty", + "enchanted", + "enchanting", + "energetic", + "enlightened", + "enormous", + "enraged", + "entire", + "envious", + "equal", + "equatorial", + "essential", + "esteemed", + "ethical", + "euphoric", + "even", + "evergreen", + "everlasting", + "every", + "evil", + "exalted", + "excellent", + "exemplary", + "exhausted", + "excitable", + "excited", + "exciting", + "exotic", + "expensive", + "experienced", + "expert", + "extraneous", + "extroverted", + "extra-large", + "extra-small", + "fabulous", + "failing", + "faint", + "fair", + "faithful", + "fake", + "false", + "familiar", + "famous", + "fancy", + "fantastic", + "far", + "faraway", + "far-flung", + "far-off", + "fast", + "fat", + "fatal", + "fatherly", + "favorable", + "favorite", + "fearful", + "fearless", + "feisty", + "feline", + "female", + "feminine", + "few", + "fickle", + "filthy", + "fine", + "finished", + "firm", + "first", + "firsthand", + "fitting", + "fixed", + "flaky", + "flamboyant", + "flashy", + "flat", + "flawed", + "flawless", + "flickering", + "flimsy", + "flippant", + "flowery", + "fluffy", + "fluid", + "flustered", + "focused", + "fond", + "foolhardy", + "foolish", + "forceful", + "forked", + "formal", + "forsaken", + "forthright", + "fortunate", + "fragrant", + "frail", + "frank", + "frayed", + "free", + "French", + "fresh", + "frequent", + "friendly", + "frightened", + "frightening", + "frigid", + "frilly", + "frizzy", + "frivolous", + "front", + "frosty", + "frozen", + "frugal", + "fruitful", + "full", + "fumbling", + "functional", + "funny", + "fussy", + "fuzzy", + "gargantuan", + "gaseous", + "general", + "generous", + "gentle", + "genuine", + "giant", + "giddy", + "gigantic", + "gifted", + "giving", + "glamorous", + "glaring", + "glass", + "gleaming", + "gleeful", + "glistening", + "glittering", + "gloomy", + "glorious", + "glossy", + "glum", + "golden", + "good", + "good-natured", + "gorgeous", + "graceful", + "gracious", + "grand", + "grandiose", + "granular", + "grateful", + "grave", + "gray", + "great", + "greedy", + "green", + "gregarious", + "grim", + "grimy", + "gripping", + "grizzled", + "gross", + "grotesque", + "grouchy", + "grounded", + "growing", + "growling", + "grown", + "grubby", + "gruesome", + "grumpy", + "guilty", + "gullible", + "gummy", + "hairy", + "half", + "handmade", + "handsome", + "handy", + "happy", + "happy-go-lucky", + "hard", + "hard-to-find", + "harmful", + "harmless", + "harmonious", + "harsh", + "hasty", + "hateful", + "haunting", + "healthy", + "heartfelt", + "hearty", + "heavenly", + "heavy", + "hefty", + "helpful", + "helpless", + "hidden", + "hideous", + "high", + "high-level", + "hilarious", + "hoarse", + "hollow", + "homely", + "honest", + "honorable", + "honored", + "hopeful", + "horrible", + "hospitable", + "hot", + "huge", + "humble", + "humiliating", + "humming", + "humongous", + "hungry", + "hurtful", + "husky", + "icky", + "icy", + "ideal", + "idealistic", + "identical", + "idle", + "idiotic", + "idolized", + "ignorant", + "ill", + "illegal", + "ill-fated", + "ill-informed", + "illiterate", + "illustrious", + "imaginary", + "imaginative", + "immaculate", + "immaterial", + "immediate", + "immense", + "impassioned", + "impeccable", + "impartial", + "imperfect", + "imperturbable", + "impish", + "impolite", + "important", + "impossible", + "impractical", + "impressionable", + "impressive", + "improbable", + "impure", + "inborn", + "incomparable", + "incompatible", + "incomplete", + "inconsequential", + "incredible", + "indelible", + "inexperienced", + "indolent", + "infamous", + "infantile", + "infatuated", + "inferior", + "infinite", + "informal", + "innocent", + "insecure", + "insidious", + "insignificant", + "insistent", + "instructive", + "insubstantial", + "intelligent", + "intent", + "intentional", + "interesting", + "internal", + "international", + "intrepid", + "ironclad", + "irresponsible", + "irritating", + "itchy", + "jaded", + "jagged", + "jam-packed", + "jaunty", + "jealous", + "jittery", + "joint", + "jolly", + "jovial", + "joyful", + "joyous", + "jubilant", + "judicious", + "juicy", + "jumbo", + "junior", + "jumpy", + "juvenile", + "kaleidoscopic", + "keen", + "key", + "kind", + "kindhearted", + "kindly", + "klutzy", + "knobby", + "knotty", + "knowledgeable", + "knowing", + "known", + "kooky", + "kosher", + "lame", + "lanky", + "large", + "last", + "lasting", + "late", + "lavish", + "lawful", + "lazy", + "leading", + "lean", + "leafy", + "left", + "legal", + "legitimate", + "light", + "lighthearted", + "likable", + "likely", + "limited", + "limp", + "limping", + "linear", + "lined", + "liquid", + "little", + "live", + "lively", + "livid", + "loathsome", + "lone", + "lonely", + "long", + "long-term", + "loose", + "lopsided", + "lost", + "loud", + "lovable", + "lovely", + "loving", + "low", + "loyal", + "lucky", + "lumbering", + "luminous", + "lumpy", + "lustrous", + "luxurious", + "mad", + "made-up", + "magnificent", + "majestic", + "major", + "male", + "mammoth", + "married", + "marvelous", + "masculine", + "massive", + "mature", + "meager", + "mealy", + "mean", + "measly", + "meaty", + "medical", + "mediocre", + "medium", + "meek", + "mellow", + "melodic", + "memorable", + "menacing", + "merry", + "messy", + "metallic", + "mild", + "milky", + "mindless", + "miniature", + "minor", + "minty", + "miserable", + "miserly", + "misguided", + "misty", + "mixed", + "modern", + "modest", + "moist", + "monstrous", + "monthly", + "monumental", + "moral", + "mortified", + "motherly", + "motionless", + "mountainous", + "muddy", + "muffled", + "multicolored", + "mundane", + "murky", + "mushy", + "musty", + "muted", + "mysterious", + "naive", + "narrow", + "nasty", + "natural", + "naughty", + "nautical", + "near", + "neat", + "necessary", + "needy", + "negative", + "neglected", + "negligible", + "neighboring", + "nervous", + "new", + "next", + "nice", + "nifty", + "nimble", + "nippy", + "nocturnal", + "noisy", + "nonstop", + "normal", + "notable", + "noted", + "noteworthy", + "novel", + "noxious", + "numb", + "nutritious", + "nutty", + "obedient", + "obese", + "oblong", + "oily", + "oblong", + "obvious", + "occasional", + "odd", + "oddball", + "offbeat", + "offensive", + "official", + "old", + "old-fashioned", + "only", + "open", + "optimal", + "optimistic", + "opulent", + "orange", + "orderly", + "organic", + "ornate", + "ornery", + "ordinary", + "original", + "other", + "our", + "outlying", + "outgoing", + "outlandish", + "outrageous", + "outstanding", + "oval", + "overcooked", + "overdue", + "overjoyed", + "overlooked", + "palatable", + "pale", + "paltry", + "parallel", + "parched", + "partial", + "passionate", + "past", + "pastel", + "peaceful", + "peppery", + "perfect", + "perfumed", + "periodic", + "perky", + "personal", + "pertinent", + "pesky", + "pessimistic", + "petty", + "phony", + "physical", + "piercing", + "pink", + "pitiful", + "plain", + "plaintive", + "plastic", + "playful", + "pleasant", + "pleased", + "pleasing", + "plump", + "plush", + "polished", + "polite", + "political", + "pointed", + "pointless", + "poised", + "poor", + "popular", + "portly", + "posh", + "positive", + "possible", + "potable", + "powerful", + "powerless", + "practical", + "precious", + "present", + "prestigious", + "pretty", + "precious", + "previous", + "pricey", + "prickly", + "primary", + "prime", + "pristine", + "private", + "prize", + "probable", + "productive", + "profitable", + "profuse", + "proper", + "proud", + "prudent", + "punctual", + "pungent", + "puny", + "pure", + "purple", + "pushy", + "putrid", + "puzzled", + "puzzling", + "quaint", + "qualified", + "quarrelsome", + "quarterly", + "queasy", + "querulous", + "questionable", + "quick", + "quick-witted", + "quiet", + "quintessential", + "quirky", + "quixotic", + "quizzical", + "radiant", + "ragged", + "rapid", + "rare", + "rash", + "raw", + "recent", + "reckless", + "rectangular", + "ready", + "real", + "realistic", + "reasonable", + "red", + "reflecting", + "regal", + "regular", + "reliable", + "relieved", + "remarkable", + "remorseful", + "remote", + "repentant", + "required", + "respectful", + "responsible", + "repulsive", + "revolving", + "rewarding", + "rich", + "rigid", + "right", + "ringed", + "ripe", + "roasted", + "robust", + "rosy", + "rotating", + "rotten", + "rough", + "round", + "rowdy", + "royal", + "rubbery", + "rundown", + "ruddy", + "rude", + "runny", + "rural", + "rusty", + "sad", + "safe", + "salty", + "same", + "sandy", + "sane", + "sarcastic", + "sardonic", + "satisfied", + "scaly", + "scarce", + "scared", + "scary", + "scented", + "scholarly", + "scientific", + "scornful", + "scratchy", + "scrawny", + "second", + "secondary", + "second-hand", + "secret", + "self-assured", + "self-reliant", + "selfish", + "sentimental", + "separate", + "serene", + "serious", + "serpentine", + "several", + "severe", + "shabby", + "shadowy", + "shady", + "shallow", + "shameful", + "shameless", + "sharp", + "shimmering", + "shiny", + "shocked", + "shocking", + "shoddy", + "short", + "short-term", + "showy", + "shrill", + "shy", + "sick", + "silent", + "silky", + "silly", + "silver", + "similar", + "simple", + "simplistic", + "sinful", + "single", + "sizzling", + "skeletal", + "skinny", + "sleepy", + "slight", + "slim", + "slimy", + "slippery", + "slow", + "slushy", + "small", + "smart", + "smoggy", + "smooth", + "smug", + "snappy", + "snarling", + "sneaky", + "sniveling", + "snoopy", + "sociable", + "soft", + "soggy", + "solid", + "somber", + "some", + "spherical", + "sophisticated", + "sore", + "sorrowful", + "soulful", + "soupy", + "sour", + "Spanish", + "sparkling", + "sparse", + "specific", + "spectacular", + "speedy", + "spicy", + "spiffy", + "spirited", + "spiteful", + "splendid", + "spotless", + "spotted", + "spry", + "square", + "squeaky", + "squiggly", + "stable", + "staid", + "stained", + "stale", + "standard", + "starchy", + "stark", + "starry", + "steep", + "sticky", + "stiff", + "stimulating", + "stingy", + "stormy", + "straight", + "strange", + "steel", + "strict", + "strident", + "striking", + "striped", + "strong", + "studious", + "stunning", + "stupendous", + "stupid", + "sturdy", + "stylish", + "subdued", + "submissive", + "substantial", + "subtle", + "suburban", + "sudden", + "sugary", + "sunny", + "super", + "superb", + "superficial", + "superior", + "supportive", + "sure-footed", + "surprised", + "suspicious", + "svelte", + "sweaty", + "sweet", + "sweltering", + "swift", + "sympathetic", + "tall", + "talkative", + "tame", + "tan", + "tangible", + "tart", + "tasty", + "tattered", + "taut", + "tedious", + "teeming", + "tempting", + "tender", + "tense", + "tepid", + "terrible", + "terrific", + "testy", + "thankful", + "that", + "these", + "thick", + "thin", + "third", + "thirsty", + "this", + "thorough", + "thorny", + "those", + "thoughtful", + "threadbare", + "thrifty", + "thunderous", + "tidy", + "tight", + "timely", + "tinted", + "tiny", + "tired", + "torn", + "total", + "tough", + "traumatic", + "treasured", + "tremendous", + "tragic", + "trained", + "tremendous", + "triangular", + "tricky", + "trifling", + "trim", + "trivial", + "troubled", + "true", + "trusting", + "trustworthy", + "trusty", + "truthful", + "tubby", + "turbulent", + "twin", + "ugly", + "ultimate", + "unacceptable", + "unaware", + "uncomfortable", + "uncommon", + "unconscious", + "understated", + "unequaled", + "uneven", + "unfinished", + "unfit", + "unfolded", + "unfortunate", + "unhappy", + "unhealthy", + "uniform", + "unimportant", + "unique", + "united", + "unkempt", + "unknown", + "unlawful", + "unlined", + "unlucky", + "unnatural", + "unpleasant", + "unrealistic", + "unripe", + "unruly", + "unselfish", + "unsightly", + "unsteady", + "unsung", + "untidy", + "untimely", + "untried", + "untrue", + "unused", + "unusual", + "unwelcome", + "unwieldy", + "unwilling", + "unwitting", + "unwritten", + "upbeat", + "upright", + "upset", + "urban", + "usable", + "used", + "useful", + "useless", + "utilized", + "utter", + "vacant", + "vague", + "vain", + "valid", + "valuable", + "vapid", + "variable", + "vast", + "velvety", + "venerated", + "vengeful", + "verifiable", + "vibrant", + "vicious", + "victorious", + "vigilant", + "vigorous", + "villainous", + "violet", + "violent", + "virtual", + "virtuous", + "visible", + "vital", + "vivacious", + "vivid", + "voluminous", + "wan", + "warlike", + "warm", + "warmhearted", + "warped", + "wary", + "wasteful", + "watchful", + "waterlogged", + "watery", + "wavy", + "wealthy", + "weak", + "weary", + "webbed", + "wee", + "weekly", + "weepy", + "weighty", + "weird", + "welcome", + "well-documented", + "well-groomed", + "well-informed", + "well-lit", + "well-made", + "well-off", + "well-to-do", + "well-worn", + "wet", + "which", + "whimsical", + "whirlwind", + "whispered", + "white", + "whole", + "whopping", + "wicked", + "wide", + "wide-eyed", + "wiggly", + "wild", + "willing", + "wilted", + "winding", + "windy", + "winged", + "wiry", + "wise", + "witty", + "wobbly", + "woeful", + "wonderful", + "wooden", + "woozy", + "wordy", + "worldly", + "worn", + "worried", + "worrisome", + "worse", + "worst", + "worthless", + "worthwhile", + "worthy", + "wrathful", + "wretched", + "writhing", + "wrong", + "wry", + "yawning", + "yearly", + "yellow", + "yellowish", + "young", + "youthful", + "yummy", + "zany", + "zealous", + "zesty", + "zigzag", +] + +export function getRandomUsername(): string { + const n = Math.random() * noun.length + const a = Math.random() * adj.length + return `tmp-${a}-${n}` +} + +export function getRandomPassword(): string { + return encodeCrock(getRandomBytes(16)) +} \ No newline at end of file diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 4ce0f140e..c13b9a3cb 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -16,11 +16,12 @@ import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; import { + ErrorNotification, ErrorType, HttpError, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { ErrorMessage } from "./hooks/notification.js"; + /** * Validate (the number part of) an amount. If needed, @@ -120,11 +121,12 @@ export function buildRequestErrorMessage( onClientError?: (status: HttpStatusCode) => TranslatedString | undefined; onServerError?: (status: HttpStatusCode) => TranslatedString | undefined; } = {}, -): ErrorMessage { - let result: ErrorMessage; +): ErrorNotification { + let result: ErrorNotification; switch (cause.type) { case ErrorType.TIMEOUT: { result = { + type: "error", title: i18n.str`Request timeout`, }; break; @@ -133,8 +135,9 @@ export function buildRequestErrorMessage( const title = specialCases.onClientError && specialCases.onClientError(cause.status); result = { + type: "error", 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), }; break; @@ -143,24 +146,27 @@ export function buildRequestErrorMessage( const title = specialCases.onServerError && specialCases.onServerError(cause.status); result = { + type: "error", title: title ? title : 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), }; break; } case ErrorType.UNREADABLE: { result = { + type: "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), }; break; } case ErrorType.UNEXPECTED: { result = { + type: "error", title: i18n.str`Unexpected error`, debug: JSON.stringify(cause), }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 295074f61..392f34981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -877,6 +877,9 @@ importers: '@gnu-taler/taler-util': specifier: workspace:* version: link:../taler-util + '@heroicons/react': + specifier: ^2.0.17 + version: 2.0.17(react@18.2.0) '@linaria/babel-preset': specifier: 4.4.5 version: 4.4.5 @@ -907,6 +910,9 @@ importers: chokidar: specifier: ^3.5.3 version: 3.5.3 + date-fns: + specifier: 2.29.3 + version: 2.29.3 esbuild: specifier: ^0.17.7 version: 0.17.7 @@ -4872,7 +4878,6 @@ packages: react: '>= 16' dependencies: react: 18.2.0 - dev: false /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} @@ -8680,7 +8685,6 @@ packages: /date-fns@2.29.3: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - dev: false /date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} -- cgit v1.2.3 From 7d4c5a71aaa6c4e781af124fe821d8be4ed101ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 20 Sep 2023 16:10:32 -0300 Subject: more ui --- packages/demobank-ui/src/hooks/backend.ts | 16 +- packages/demobank-ui/src/hooks/settings.ts | 10 ++ packages/demobank-ui/src/pages/BankFrame.tsx | 50 +++++-- packages/demobank-ui/src/pages/HomePage.tsx | 9 +- .../src/pages/WithdrawalConfirmationQuestion.tsx | 165 +++++++++++---------- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 117 ++++++++++----- 6 files changed, 221 insertions(+), 146 deletions(-) (limited to 'packages/demobank-ui/src/pages/HomePage.tsx') diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 4b60d1b6c..c05ab33e9 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -85,18 +85,26 @@ export function getInitialBackendBaseURL(): string { typeof localStorage !== "undefined" ? localStorage.getItem("bank-base-url") : undefined; + let result: string; if (!overrideUrl) { //normal path if (!bankUiSettings.backendBaseURL) { console.error( "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); - return canonicalizeBaseUrl(window.origin); + result = window.origin } - return canonicalizeBaseUrl(bankUiSettings.backendBaseURL); + result = bankUiSettings.backendBaseURL; + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) } - // testing/development path - return canonicalizeBaseUrl(overrideUrl); } export const defaultState: BackendState = { diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 46b31bf2a..43e803726 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -15,8 +15,12 @@ */ import { + AmountString, Codec, buildCodecForObject, + codecForAmountString, + codecForBoolean, + codecForNumber, codecForString, codecOptional, } from "@gnu-taler/taler-util"; @@ -24,15 +28,21 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; interface Settings { currentWithdrawalOperationId: string | undefined; + showWithdrawalSuccess: boolean; + maxWithdrawalAmount: number; } export const codecForSettings = (): Codec => buildCodecForObject() .property("currentWithdrawalOperationId", codecOptional(codecForString())) + .property("showWithdrawalSuccess", (codecForBoolean())) + .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, + showWithdrawalSuccess: true, + maxWithdrawalAmount: 25 }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index d234845a0..e682085ae 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -183,8 +183,28 @@ export function BankFrame({ {/* */} +
  • +
    + + + Show withdrawal confirmation + + + +
    +
  • + + +
  • Sites @@ -343,14 +363,14 @@ function StatusBanner(): VNode { switch (n.message.type) { case "error": return
    -
    -
    - -
    -
    -

    {n.message.title}

    +
    +
    + +
    +
    +

    {n.message.title}

    -

    +

    +
    + {n.message.description && +
    + {n.message.description} +
    + }
    - {n.message.description && -
    - {n.message.description} -
    - } -
    case "info": return
    diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index e00daf278..e82e46eb2 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -95,8 +95,9 @@ export function WithdrawalOperationPage({ }): VNode { //FIXME: libeufin sandbox should return show to create the integration api endpoint //or return withdrawal uri from response + const baseUrl = getInitialBackendBaseURL() const uri = stringifyWithdrawUri({ - bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`, + bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`, withdrawalOperationId: operationId, }); const parsedUri = parseWithdrawUri(uri); @@ -155,7 +156,7 @@ export function handleNotOkResult( } case ErrorType.SERVER: { notify({ - type: "error", + type: "error", title: i18n.str`Server returned with error`, description: result.payload.error.description as TranslatedString, debug: JSON.stringify(result.payload), @@ -164,7 +165,7 @@ export function handleNotOkResult( } case ErrorType.UNREADABLE: { notify({ - type:"error", + type: "error", title: i18n.str`Unexpected error.`, description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`, debug: JSON.stringify(result), @@ -173,7 +174,7 @@ export function handleNotOkResult( } case ErrorType.UNEXPECTED: { notify({ - type:"error", + type: "error", title: i18n.str`Unexpected error.`, description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`, debug: JSON.stringify(result), diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 28f00169d..80e7a78ac 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -20,24 +20,23 @@ import { HttpStatusCode, Logger, PaytoUri, - PaytoUriGeneric, PaytoUriIBAN, PaytoUriTalerBank, TranslatedString, - WithdrawUriResult, + WithdrawUriResult } from "@gnu-taler/taler-util"; import { RequestError, notify, notifyError, + notifyInfo, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; +import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAccessAnonAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { Amount } from "./PaytoWireTransferForm.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -71,6 +70,7 @@ export function WithdrawalConfirmationQuestion({ const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI(); const [captchaAnswer, setCaptchaAnswer] = useState(); const answer = parseInt(captchaAnswer ?? "", 10); + const [busy, setBusy] = useState>() const errors = undefinedIfEmpty({ answer: !captchaAnswer ? i18n.str`Answer the question before continue` @@ -79,13 +79,15 @@ export function WithdrawalConfirmationQuestion({ : answer !== captchaNumbers.a + captchaNumbers.b ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` : undefined, - }); + }) ?? busy; async function doTransfer() { try { + setBusy({}) await confirmWithdrawal( withdrawUri.withdrawalOperationId, ); + notifyInfo(i18n.str`Wire transfer completed!`) } catch (error) { if (error instanceof RequestError) { notify( @@ -107,10 +109,12 @@ export function WithdrawalConfirmationQuestion({ ) } } + setBusy(undefined) } async function doCancel() { try { + setBusy({}) await abortWithdrawal(withdrawUri.withdrawalOperationId); onAborted(); } catch (error) { @@ -132,6 +136,7 @@ export function WithdrawalConfirmationQuestion({ ) } } + setBusy(undefined) } return ( @@ -142,68 +147,6 @@ export function WithdrawalConfirmationQuestion({ Confirm the withdrawal operation
    -
    -
    -
    -

    Wire transfer details

    -
    -
    -
    - {((): VNode => { - switch (details.account.targetType) { - case "iban": { - const p = details.account as PaytoUriIBAN - const name = p.params["receiver-name"] - return -
    -
    Exchange account
    -
    {p.iban}
    -
    - {name && -
    -
    Exchange name
    -
    {p.params["receiver-name"]}
    -
    - } -
    - } - case "x-taler-bank": { - const p = details.account as PaytoUriTalerBank - const name = p.params["receiver-name"] - return -
    -
    Exchange account
    -
    {p.account}
    -
    - {name && -
    -
    Exchange name
    -
    {p.params["receiver-name"]}
    -
    - } -
    - } - default: - return
    -
    Exchange account
    -
    {details.account.targetPath}
    -
    - - } - })()} -
    -
    Withdrawal identification
    -
    {details.reserve}
    -
    -
    -
    Amount
    -
    {Amounts.stringifyValue(details.amount)}
    -
    -
    -
    -
    - -
    - +
    @@ -323,6 +262,68 @@ export function WithdrawalConfirmationQuestion({
    +
    +
    +
    +

    Wire transfer details

    +
    +
    +
    + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.iban}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return +
    +
    Exchange account
    +
    {p.account}
    +
    + {name && +
    +
    Exchange name
    +
    {p.params["receiver-name"]}
    +
    + } +
    + } + default: + return
    +
    Exchange account
    +
    {details.account.targetPath}
    +
    + + } + })()} +
    +
    Withdrawal identification
    +
    {details.reserve}
    +
    +
    +
    Amount
    +
    {Amounts.stringifyValue(details.amount)}
    +
    +
    +
    +
    + +
    diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 3b983c2d4..b48e3b1dc 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -62,9 +62,10 @@ export function WithdrawalQRCode({ result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound ) { + clearCurrentWithdrawal() return
    operation not found
    ; } - onLoadNotOk(); + // onLoadNotOk(); return handleNotOkResult(i18n)(result); } const { data } = result; @@ -85,12 +86,12 @@ export function WithdrawalQRCode({

    { e.preventDefault(); clearCurrentWithdrawal() onContinue() - }}> + }}> {i18n.str`Continue`} @@ -99,49 +100,69 @@ export function WithdrawalQRCode({ } if (data.confirmation_done) { - return
    -

    {i18n.str`Operation completed`}

    - -
    -

    - - The wire transfer to the GNU Taler Exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. - -

    -

    - - You can close this page now or continue to the account page. - -

    -
    + if (!settings.showWithdrawalSuccess) { + clearCurrentWithdrawal() + onContinue() + } + return
    +
    +
    + +
    +
    + +
    +

    + + The wire transfer to the Taler exchange bank's account is completed, now the + exchange will send the requested amount into your GNU Taler wallet. + +

    +
    +
    +

    + + You can close this page now or continue to the account page. + +

    +
    +
    +
    +
    +
    + + + Do not show this again + + +
    -
    -
    - } - if (!data.selected_reserve_pub) { - return
    - the exchange is selcted but no reserve pub +
    +
    + +
  • - } - const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) - if (!account) { - return
    - the exchange is selcted but no account -
    } - if (!data.selection_done) { return ( ); } + if (!data.selected_reserve_pub) { + return
    + the exchange is selcted but no reserve pub +
    + } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return
    + the exchange is selcted but no account +
    + } + return ( ); } \ No newline at end of file -- cgit v1.2.3 From 062939d9cc016a186a282f7a48492c3e01cd740c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 10:31:10 -0300 Subject: admin refactor --- packages/demobank-ui/src/components/Routing.tsx | 13 +- packages/demobank-ui/src/pages/AdminPage.tsx | 1042 -------------------- packages/demobank-ui/src/pages/BusinessAccount.tsx | 758 -------------- packages/demobank-ui/src/pages/HomePage.tsx | 19 +- .../demobank-ui/src/pages/ShowAccountDetails.tsx | 143 +++ .../src/pages/UpdateAccountPassword.tsx | 131 +++ .../src/pages/WithdrawalConfirmationQuestion.tsx | 3 +- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 4 - packages/demobank-ui/src/pages/admin/Account.tsx | 56 ++ .../demobank-ui/src/pages/admin/AccountForm.tsx | 219 ++++ .../demobank-ui/src/pages/admin/AccountList.tsx | 120 +++ .../src/pages/admin/CreateNewAccount.tsx | 107 ++ packages/demobank-ui/src/pages/admin/Home.tsx | 162 +++ .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 112 +++ packages/demobank-ui/src/pages/business/Home.tsx | 757 ++++++++++++++ 15 files changed, 1823 insertions(+), 1823 deletions(-) delete mode 100644 packages/demobank-ui/src/pages/AdminPage.tsx delete mode 100644 packages/demobank-ui/src/pages/BusinessAccount.tsx create mode 100644 packages/demobank-ui/src/pages/ShowAccountDetails.tsx create mode 100644 packages/demobank-ui/src/pages/UpdateAccountPassword.tsx create mode 100644 packages/demobank-ui/src/pages/admin/Account.tsx create mode 100644 packages/demobank-ui/src/pages/admin/AccountForm.tsx create mode 100644 packages/demobank-ui/src/pages/admin/AccountList.tsx create mode 100644 packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx create mode 100644 packages/demobank-ui/src/pages/admin/Home.tsx create mode 100644 packages/demobank-ui/src/pages/admin/RemoveAccount.tsx create mode 100644 packages/demobank-ui/src/pages/business/Home.tsx (limited to 'packages/demobank-ui/src/pages/HomePage.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index 890058a9b..ef11af76e 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -19,14 +19,14 @@ import { VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; import { useEffect } from "preact/hooks"; import { BankFrame } from "../pages/BankFrame.js"; -import { BusinessAccount } from "../pages/BusinessAccount.js"; +import { BusinessAccount } from "../pages/business/Home.js"; import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js"; import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js"; import { RegistrationPage } from "../pages/RegistrationPage.js"; import { Test } from "../pages/Test.js"; import { useBackendContext } from "../context/backend.js"; import { LoginForm } from "../pages/LoginForm.js"; -import { AdminPage } from "../pages/AdminPage.js"; +import { AdminHome } from "../pages/admin/Home.js"; export function Routing(): VNode { const history = createHashHistory(); @@ -34,6 +34,7 @@ export function Routing(): VNode { if (backend.state.status === "loggedOut") { return { route("/business"); }} @@ -63,7 +64,7 @@ export function Routing(): VNode { } - const isAdmin = backend.state.isUserAdministrator + const { isUserAdministrator, username } = backend.state return ( { - if (isAdmin) { - return { route("/register"); }} />; } else { return { route(`/operation/${wopid}`); }} @@ -130,6 +132,7 @@ export function Routing(): VNode { path="/business" component={() => ( { route("/account"); }} diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx deleted file mode 100644 index 18462bdc3..000000000 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ /dev/null @@ -1,1042 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpResponsePaginated, - RequestError, - notify, - notifyError, - notifyInfo, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Cashouts } from "../components/Cashouts/index.js"; -import { useBackendContext } from "../context/backend.js"; -import { useAccountDetails } from "../hooks/access.js"; -import { - useAdminAccountAPI, - useBusinessAccountDetails, - useBusinessAccounts, -} from "../hooks/circuit.js"; -import { - buildRequestErrorMessage, - PartialButDefined, - RecursivePartial, - undefinedIfEmpty, - validateIBAN, - WithIntermediate, -} from "../utils.js"; -import { ShowCashoutDetails } from "./BusinessAccount.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; - -const charset = - "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const upperIdx = charset.indexOf("A"); - -function randomPassword(): string { - const random = Array.from({ length: 16 }).map(() => { - return charset.charCodeAt(Math.random() * charset.length); - }); - // first char can't be upper - const charIdx = charset.indexOf(String.fromCharCode(random[0])); - random[0] = - charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0]; - return String.fromCharCode(...random); -} - -interface Props { - onRegister: () => void; -} -/** - * Query account information and show QR code if there is pending withdrawal - */ -export function AdminPage({ onRegister }: Props): VNode { - const [account, setAccount] = useState(); - const [showDetails, setShowDetails] = useState(); - const [showCashouts, setShowCashouts] = useState(); - const [updatePassword, setUpdatePassword] = useState(); - const [removeAccount, setRemoveAccount] = useState(); - const [showCashoutDetails, setShowCashoutDetails] = useState< - string | undefined - >(); - - const [createAccount, setCreateAccount] = useState(false); - - const result = useBusinessAccounts({ account }); - const { i18n } = useTranslationContext(); - - if (result.loading) return
    ; - if (!result.ok) { - return handleNotOkResult(i18n, onRegister)(result); - } - - const { customers } = result.data; - - if (showCashoutDetails) { - return ( - { - setShowCashoutDetails(undefined); - }} - /> - ); - } - - if (showCashouts) { - return ( -
    -
    -

    - Cashout for account {showCashouts} -

    -
    - { - setShowCashouts(id); - setShowCashouts(undefined); - }} - /> -

    - { - e.preventDefault(); - setShowCashouts(undefined); - }} - /> -

    -
    - ); - } - - if (showDetails) { - return ( - { - setUpdatePassword(showDetails); - setShowDetails(undefined); - }} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Account updated`); - setShowDetails(undefined); - }} - onClear={() => { - setShowDetails(undefined); - }} - /> - ); - } - if (removeAccount) { - return ( - { - notifyInfo(i18n.str`Account removed`); - setRemoveAccount(undefined); - }} - onClear={() => { - setRemoveAccount(undefined); - }} - /> - ); - } - if (updatePassword) { - return ( - { - notifyInfo(i18n.str`Password changed`); - setUpdatePassword(undefined); - }} - onClear={() => { - setUpdatePassword(undefined); - }} - /> - ); - } - if (createAccount) { - return ( - setCreateAccount(false)} - onCreateSuccess={(password) => { - notifyInfo( - i18n.str`Account created with password "${password}". The user must change the password on the next login.`, - ); - setCreateAccount(false); - }} - /> - ); - } - - return ( - -
    -

    - Admin panel -

    -
    - -

    -

    -
    -
    - { - e.preventDefault(); - - setCreateAccount(true); - }} - /> -
    -
    -

    - - -
    - {!customers.length ? ( -
    - ) : ( -
    -

    {i18n.str`Accounts:`}

    -
    -
    {i18n.str`Date`}{i18n.str`Amount`}{i18n.str`Counterpart`}{i18n.str`Subject`}{i18n.str`Date`}{i18n.str`Amount`}{i18n.str`Counterpart`}{i18n.str`Subject`}
    +
    {item.when.t_ms === "never" ? "" : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
    @@ -66,7 +66,7 @@ export function ReadyView({ transactions }: State.Ready): VNode { <{i18n.str`invalid value`}> )}
    {item.counterpart}{item.subject}{item.subject}
    - - - - - - - - - - {customers.map((item, idx) => { - const balance = !item.balance - ? undefined - : Amounts.parse(item.balance.amount); - const balanceIsDebit = - item.balance && - item.balance.credit_debit_indicator == "debit"; - return ( - - - - - - - ); - })} - -
    {i18n.str`Username`}{i18n.str`Name`}{i18n.str`Balance`}{i18n.str`Actions`}
    - { - e.preventDefault(); - setShowDetails(item.username); - }} - > - {item.username} - - {item.name} - {!balance ? ( - i18n.str`unknown` - ) : ( - - {balanceIsDebit ? - : null} - {`${Amounts.stringifyValue( - balance, - )}`} -   - {`${balance.currency}`} - - )} - - { - e.preventDefault(); - setUpdatePassword(item.username); - }} - > - change password - -   - { - e.preventDefault(); - setShowCashouts(item.username); - }} - > - cashouts - -   - { - e.preventDefault(); - setRemoveAccount(item.username); - }} - > - remove - -
    -
    - - )} - - - ); -} - -function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { - const { i18n } = useTranslationContext(); - const r = useBackendContext(); - const account = r.state.status === "loggedIn" ? r.state.username : "admin"; - const result = useAccountDetails(account); - - if (!result.ok) { - return handleNotOkResult(i18n, onRegister)(result); - } - const { data } = result; - const balance = Amounts.parseOrThrow(data.balance.amount); - const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); - const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; - const limit = balanceIsDebit - ? Amounts.sub(debitThreshold, balance).amount - : Amounts.add(balance, debitThreshold).amount; - if (!balance) return ; - return ( - -
    -
    -

    {i18n.str`Bank account balance`}

    - {!balance ? ( -
    - Waiting server response... -
    - ) : ( -
    - {balanceIsDebit ? - : null} - {`${Amounts.stringifyValue(balance)}`} -   - {`${balance.currency}`} -
    - )} -
    -
    - { - notifyInfo(i18n.str`Wire transfer created!`); - }} - onCancel={undefined} - /> -
    - ); -} - -const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; -const EMAIL_REGEX = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; - -function initializeFromTemplate( - account: SandboxBackend.Circuit.CircuitAccountData | undefined, -): WithIntermediate { - const emptyAccount = { - cashout_address: undefined, - iban: undefined, - name: undefined, - username: undefined, - contact_data: undefined, - }; - const emptyContact = { - email: undefined, - phone: undefined, - }; - - const initial: PartialButDefined = - structuredClone(account) ?? emptyAccount; - if (typeof initial.contact_data === "undefined") { - initial.contact_data = emptyContact; - } - initial.contact_data.email; - return initial as any; -} - -export function UpdateAccountPassword({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, -}: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; - onClear: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { changePassword } = useAdminAccountAPI(); - const [password, setPassword] = useState(); - const [repeat, setRepeat] = useState(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
    account not found
    ; - } - return onLoadNotOk(result); - } - - const errors = undefinedIfEmpty({ - password: !password ? i18n.str`required` : undefined, - repeat: !repeat - ? i18n.str`required` - : password !== repeat - ? i18n.str`password doesn't match` - : undefined, - }); - - return ( -
    -
    -

    - Update password for {account} -

    -
    - -
    -
    -
    - - { - setPassword(e.currentTarget.value); - }} - /> - -
    -
    - - { - setRepeat(e.currentTarget.value); - }} - /> - -
    -
    -

    -

    -
    - { - e.preventDefault(); - onClear(); - }} - /> -
    -
    - { - e.preventDefault(); - if (!!errors || !password) return; - try { - const r = await changePassword(account, { - new_password: password, - }); - onUpdateSuccess(); - } catch (error) { - if (error instanceof RequestError) { - notify(buildRequestErrorMessage(i18n, error.cause)); - } else { - notifyError(i18n.str`Operation failed, please report`, (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString) - } - } - }} - /> -
    -
    -

    -
    -
    - ); -} - -function CreateNewAccount({ - onClose, - onCreateSuccess, -}: { - onClose: () => void; - onCreateSuccess: (password: string) => void; -}): VNode { - const { i18n } = useTranslationContext(); - const { createAccount } = useAdminAccountAPI(); - const [submitAccount, setSubmitAccount] = useState< - SandboxBackend.Circuit.CircuitAccountData | undefined - >(); - return ( -
    -
    -

    - New account -

    -
    - -
    - { - setSubmitAccount(a); - }} - /> - -

    -

    -
    - { - e.preventDefault(); - onClose(); - }} - /> -
    -
    - { - e.preventDefault(); - - if (!submitAccount) return; - try { - const account: SandboxBackend.Circuit.CircuitAccountRequest = - { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, - internal_iban: submitAccount.iban, - name: submitAccount.name, - username: submitAccount.username, - password: randomPassword(), - }; - - await createAccount(account); - onCreateSuccess(account.password); - } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The rights to perform the operation are not sufficient` - : status === HttpStatusCode.BadRequest - ? i18n.str`Input data was invalid` - : status === HttpStatusCode.Conflict - ? i18n.str`At least one registration detail was not available` - : undefined, - }), - ); - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } - }} - /> -
    -
    -

    -
    -
    - ); -} - -export function ShowAccountDetails({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, - onChangePassword, -}: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; - onClear?: () => void; - onChangePassword: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { updateAccount } = useAdminAccountAPI(); - const [update, setUpdate] = useState(false); - const [submitAccount, setSubmitAccount] = useState< - SandboxBackend.Circuit.CircuitAccountData | undefined - >(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
    account not found
    ; - } - return onLoadNotOk(result); - } - - return ( -
    -
    -

    - Business account details -

    -
    -
    - setSubmitAccount(a)} - /> - -
    -
    - {onClear ? ( - { - e.preventDefault(); - onClear(); - }} - /> - ) : undefined} -
    -
    -
    - { - e.preventDefault(); - onChangePassword(); - }} - /> -
    -
    - { - e.preventDefault(); - - if (!update) { - setUpdate(true); - } else { - if (!submitAccount) return; - try { - await updateAccount(account, { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, - }); - onUpdateSuccess(); - } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The rights to change the account are not sufficient` - : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : undefined, - }), - ); - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } - } - }} - /> -
    -
    -
    -

    -
    -
    - ); -} - -function RemoveAccount({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, -}: { - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; - onClear: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useAccountDetails(account); - const { deleteAccount } = useAdminAccountAPI(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return
    account not found
    ; - } - return onLoadNotOk(result); - } - - const balance = Amounts.parse(result.data.balance.amount); - if (!balance) { - return
    there was an error reading the balance
    ; - } - const isBalanceEmpty = Amounts.isZero(balance); - return ( -
    -
    -

    - Remove account: {account} -

    -
    - {/* {FXME: SHOW WARNING} */} - {/* {!isBalanceEmpty && ( - saveError(undefined)} - /> - )} */} - -

    -

    -
    - { - e.preventDefault(); - onClear(); - }} - /> -
    -
    - { - e.preventDefault(); - try { - const r = await deleteAccount(account); - onUpdateSuccess(); - } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The administrator specified a institutional username` - : status === HttpStatusCode.NotFound - ? i18n.str`The username was not found` - : status === HttpStatusCode.PreconditionFailed - ? i18n.str`Balance was not zero` - : undefined, - }), - ); - } else { - notifyError(i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString); - } - } - }} - /> -
    -
    -

    -
    - ); -} -/** - * Create valid account object to update or create - * Take template as initial values for the form - * Purpose indicate if all field al read only (show), part of them (update) - * or none (create) - * @param param0 - * @returns - */ -function AccountForm({ - template, - purpose, - onChange, -}: { - template: SandboxBackend.Circuit.CircuitAccountData | undefined; - onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; - purpose: "create" | "update" | "show"; -}): VNode { - const initial = initializeFromTemplate(template); - const [form, setForm] = useState(initial); - const [errors, setErrors] = useState< - RecursivePartial | undefined - >(undefined); - const { i18n } = useTranslationContext(); - - function updateForm(newForm: typeof initial): void { - const parsed = !newForm.cashout_address - ? undefined - : parsePaytoUri(newForm.cashout_address); - - const errors = undefinedIfEmpty>({ - cashout_address: !newForm.cashout_address - ? i18n.str`required` - : !parsed - ? i18n.str`does not follow the pattern` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`only "IBAN" target are supported` - : !IBAN_REGEX.test(parsed.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(parsed.iban, i18n), - contact_data: undefinedIfEmpty({ - email: !newForm.contact_data?.email - ? i18n.str`required` - : !EMAIL_REGEX.test(newForm.contact_data.email) - ? i18n.str`it should be an email` - : undefined, - phone: !newForm.contact_data?.phone - ? i18n.str`required` - : !newForm.contact_data.phone.startsWith("+") - ? i18n.str`should start with +` - : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) - ? i18n.str`phone number can't have other than numbers` - : undefined, - }), - iban: !newForm.iban - ? undefined //optional field - : !IBAN_REGEX.test(newForm.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(newForm.iban, i18n), - name: !newForm.name ? i18n.str`required` : undefined, - username: !newForm.username ? i18n.str`required` : undefined, - }); - setErrors(errors); - setForm(newForm); - onChange(errors === undefined ? (newForm as any) : undefined); - } - - return ( -
    -
    - - { - form.username = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - />{" "} - -
    -
    - - { - form.name = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    - {purpose !== "create" && ( -
    - - { - form.iban = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    - )} -
    - - { - form.contact_data.email = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    -
    - - { - form.contact_data.phone = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    -
    - - { - form.cashout_address = "payto://iban/" + e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    -
    - ); -} diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx deleted file mode 100644 index ec71ceca6..000000000 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ /dev/null @@ -1,758 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ -import { - AmountJson, - Amounts, - HttpStatusCode, - TranslatedString -} from "@gnu-taler/taler-util"; -import { - HttpResponse, - HttpResponsePaginated, - RequestError, - notify, - notifyError, - notifyInfo, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { Cashouts } from "../components/Cashouts/index.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { useBackendContext } from "../context/backend.js"; -import { useAccountDetails } from "../hooks/access.js"; -import { - useCashoutDetails, - useCircuitAccountAPI, - useEstimator, - useRatiosAndFeeConfig, -} from "../hooks/circuit.js"; -import { - TanChannel, - buildRequestErrorMessage, - undefinedIfEmpty, -} from "../utils.js"; -import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { LoginForm } from "./LoginForm.js"; -import { Amount } from "./PaytoWireTransferForm.js"; - -interface Props { - onClose: () => void; - onRegister: () => void; - onLoadNotOk: () => void; -} -export function BusinessAccount({ - onClose, - onLoadNotOk, - onRegister, -}: Props): VNode { - const { i18n } = useTranslationContext(); - const backend = useBackendContext(); - const [updatePassword, setUpdatePassword] = useState(false); - const [newCashout, setNewcashout] = useState(false); - const [showCashoutDetails, setShowCashoutDetails] = useState< - string | undefined - >(); - - if (backend.state.status === "loggedOut") { - return ; - } - - if (newCashout) { - return ( - { - setNewcashout(false); - }} - onComplete={(id) => { - notifyInfo( - i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`, - ); - setNewcashout(false); - setShowCashoutDetails(id); - }} - /> - ); - } - if (showCashoutDetails) { - return ( - { - setShowCashoutDetails(undefined); - }} - /> - ); - } - if (updatePassword) { - return ( - { - notifyInfo(i18n.str`Password changed`); - setUpdatePassword(false); - }} - onClear={() => { - setUpdatePassword(false); - }} - /> - ); - } - return ( -
    - { - notifyInfo(i18n.str`Account updated`); - }} - onChangePassword={() => { - setUpdatePassword(true); - }} - onClear={onClose} - /> -
    -
    -

    {i18n.str`Latest cashouts`}

    - { - setShowCashoutDetails(id); - }} - /> -
    -
    -
    -
    - { - e.preventDefault(); - setNewcashout(true); - }} - /> -
    -
    -
    - ); -} - -interface PropsCashout { - account: string; - onComplete: (id: string) => void; - onCancel: () => void; - onLoadNotOk: ( - error: - | HttpResponsePaginated - | HttpResponse, - ) => VNode; -} - -type FormType = { - isDebit: boolean; - amount: string; - subject: string; - channel: TanChannel; -}; -type ErrorFrom = { - [P in keyof T]+?: string; -}; - -// check #7719 -function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse< - SandboxBackend.Circuit.Config & { hasChanged?: boolean }, - SandboxBackend.SandboxError -> { - const result = useRatiosAndFeeConfig(); - const [oldResult, setOldResult] = useState< - SandboxBackend.Circuit.Config | undefined - >(undefined); - const dataFromBackend = result.ok ? result.data : undefined; - useEffect(() => { - // save only the first result of /config to the backend - if (!dataFromBackend || oldResult !== undefined) return; - setOldResult(dataFromBackend); - }, [dataFromBackend]); - - if (!result.ok) return result; - - const data = !oldResult ? result.data : oldResult; - const hasChanged = - oldResult && - (result.data.name !== oldResult.name || - result.data.version !== oldResult.version || - result.data.ratios_and_fees.buy_at_ratio !== - oldResult.ratios_and_fees.buy_at_ratio || - result.data.ratios_and_fees.buy_in_fee !== - oldResult.ratios_and_fees.buy_in_fee || - result.data.ratios_and_fees.sell_at_ratio !== - oldResult.ratios_and_fees.sell_at_ratio || - result.data.ratios_and_fees.sell_out_fee !== - oldResult.ratios_and_fees.sell_out_fee || - result.data.fiat_currency !== oldResult.fiat_currency); - - return { - ...result, - data: { ...data, hasChanged }, - }; -} - -function CreateCashout({ - account, - onComplete, - onCancel, - onLoadNotOk, -}: PropsCashout): VNode { - const { i18n } = useTranslationContext(); - const ratiosResult = useRatiosAndFeeConfig(); - const result = useAccountDetails(account); - const { - estimateByCredit: calculateFromCredit, - estimateByDebit: calculateFromDebit, - } = useEstimator(); - const [form, setForm] = useState>({ isDebit: true }); - - const { createCashout } = useCircuitAccountAPI(); - if (!result.ok) return onLoadNotOk(result); - if (!ratiosResult.ok) return onLoadNotOk(ratiosResult); - const config = ratiosResult.data; - - const balance = Amounts.parseOrThrow(result.data.balance.amount); - const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); - const zero = Amounts.zeroOfCurrency(balance.currency); - const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; - const limit = balanceIsDebit - ? Amounts.sub(debitThreshold, balance).amount - : Amounts.add(balance, debitThreshold).amount; - - const zeroCalc = { debit: zero, credit: zero, beforeFee: zero }; - const [calc, setCalc] = useState(zeroCalc); - const sellRate = config.ratios_and_fees.sell_at_ratio; - const sellFee = !config.ratios_and_fees.sell_out_fee - ? zero - : Amounts.parseOrThrow( - `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`, - ); - const fiatCurrency = config.fiat_currency; - - if (!sellRate || sellRate < 0) return
    error rate
    ; - - const amount = Amounts.parseOrThrow( - `${!form.isDebit ? fiatCurrency : balance.currency}:${ - !form.amount ? "0" : form.amount - }`, - ); - - useEffect(() => { - if (form.isDebit) { - calculateFromDebit(amount, sellFee, sellRate) - .then((r) => { - setCalc(r); - }) - .catch((error) => { - notify( - error instanceof RequestError - ? buildRequestErrorMessage(i18n, error.cause) - : { - type: "error", - title: i18n.str`Could not estimate the cashout`, - description: error.message as TranslatedString - }, - ); - }); - } else { - calculateFromCredit(amount, sellFee, sellRate) - .then((r) => { - setCalc(r); - }) - .catch((error) => { - notify( - error instanceof RequestError - ? buildRequestErrorMessage(i18n, error.cause) - : { - type: "error", - title: i18n.str`Could not estimate the cashout`, - description: error.message, - }, - ); - }); - } - }, [form.amount, form.isDebit]); - - const balanceAfter = Amounts.sub(balance, calc.debit).amount; - - function updateForm(newForm: typeof form): void { - setForm(newForm); - } - const errors = undefinedIfEmpty>({ - amount: !form.amount - ? i18n.str`required` - : !amount - ? i18n.str`could not be parsed` - : Amounts.cmp(limit, calc.debit) === -1 - ? i18n.str`balance is not enough` - : Amounts.cmp(calc.beforeFee, sellFee) === -1 - ? i18n.str`the total amount to transfer does not cover the fees` - : Amounts.isZero(calc.credit) - ? i18n.str`the total transfer at destination will be zero` - : undefined, - channel: !form.channel ? i18n.str`required` : undefined, - }); - - return ( -
    -

    New cashout

    -
    -
    - - { - form.subject = e.currentTarget.value; - updateForm(structuredClone(form)); - }} - /> - -
    -
    - -
    - { - form.amount = v; - updateForm(structuredClone(form)); - }} - error={errors?.amount} - /> - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    {" "} - {Amounts.isZero(sellFee) ? undefined : ( - -
    - - -
    - -
    - - -
    -
    - )} -
    - - -
    -
    - - -
    - { - e.preventDefault(); - form.channel = TanChannel.EMAIL; - updateForm(structuredClone(form)); - }} - /> - { - e.preventDefault(); - form.channel = TanChannel.SMS; - updateForm(structuredClone(form)); - }} - /> - { - e.preventDefault(); - form.channel = TanChannel.FILE; - updateForm(structuredClone(form)); - }} - /> -
    - -
    -
    -
    - - - -
    -
    -
    - ); -} - -interface ShowCashoutProps { - id: string; - onCancel: () => void; - onLoadNotOk: ( - error: HttpResponsePaginated, - ) => VNode; -} -export function ShowCashoutDetails({ - id, - onCancel, - onLoadNotOk, -}: ShowCashoutProps): VNode { - const { i18n } = useTranslationContext(); - const result = useCashoutDetails(id); - const { abortCashout, confirmCashout } = useCircuitAccountAPI(); - const [code, setCode] = useState(undefined); - if (!result.ok) return onLoadNotOk(result); - const errors = undefinedIfEmpty({ - code: !code ? i18n.str`required` : undefined, - }); - const isPending = String(result.data.status).toUpperCase() === "PENDING"; - return ( -
    -

    Cashout details {id}

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - {isPending ? ( -
    - - { - setCode(e.currentTarget.value); - }} - /> - -
    - ) : undefined} -
    -
    -
    - - {isPending ? ( -
    - -   - -
    - ) : ( -
    - )} -
    -
    - ); -} - -const MAX_AMOUNT_DIGIT = 2; -/** - * Truncate the amount of digits to display - * in the form based on the fee calculations - * - * Backend must have the same truncation - * @param a - * @returns - */ -function truncate(a: AmountJson): AmountJson { - const str = Amounts.stringify(a); - const idx = str.indexOf("."); - if (idx === -1) { - return a; - } - const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT); - return Amounts.parseOrThrow(truncated); -} - -export function assertUnreachable(x: never): never { - throw new Error("Didn't expect to get here"); -} diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index e82e46eb2..a911f347c 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -35,7 +35,7 @@ import { useBackendContext } from "../context/backend.js"; import { getInitialBackendBaseURL } from "../hooks/backend.js"; import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; -import { AdminPage } from "./AdminPage.js"; +import { AdminHome } from "./admin/Home.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; import { error } from "console"; @@ -54,31 +54,24 @@ const logger = new Logger("AccountPage"); */ export function HomePage({ onRegister, + account, onPendingOperationFound, }: { + account: string, onPendingOperationFound: (id: string) => void; onRegister: () => void; }): VNode { - const backend = useBackendContext(); const [settings] = useSettings(); const { i18n } = useTranslationContext(); - if (backend.state.status === "loggedOut") { - return ; - } - if (settings.currentWithdrawalOperationId) { onPendingOperationFound(settings.currentWithdrawalOperationId); return ; } - if (backend.state.isUserAdministrator) { - return ; - } - return ( ); @@ -105,8 +98,8 @@ export function WithdrawalOperationPage({ if (!parsedUri) { notifyError( - i18n.str`The Withdrawal URI is not valid: "${uri}"`, - undefined + i18n.str`The Withdrawal URI is not valid`, + uri as TranslatedString ); return ; } diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx new file mode 100644 index 000000000..91b50b84c --- /dev/null +++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -0,0 +1,143 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode,h } from "preact"; +import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { buildRequestErrorMessage } from "../utils.js"; +import { AccountForm } from "./admin/AccountForm.js"; + +export function ShowAccountDetails({ + account, + onClear, + onUpdateSuccess, + onLoadNotOk, + onChangePassword, + }: { + onLoadNotOk: ( + error: HttpResponsePaginated, + ) => VNode; + onClear?: () => void; + onChangePassword: () => void; + onUpdateSuccess: () => void; + account: string; + }): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + const { updateAccount } = useAdminAccountAPI(); + const [update, setUpdate] = useState(false); + const [submitAccount, setSubmitAccount] = useState< + SandboxBackend.Circuit.CircuitAccountData | undefined + >(); + + if (!result.ok) { + if (result.loading || result.type === ErrorType.TIMEOUT) { + return onLoadNotOk(result); + } + if (result.status === HttpStatusCode.NotFound) { + return
    account not found
    ; + } + return onLoadNotOk(result); + } + + return ( +
    +
    +

    + Business account details +

    +
    +
    + setSubmitAccount(a)} + /> + +
    +
    + {onClear ? ( + { + e.preventDefault(); + onClear(); + }} + /> + ) : undefined} +
    +
    +
    + { + e.preventDefault(); + onChangePassword(); + }} + /> +
    +
    + { + e.preventDefault(); + + if (!update) { + setUpdate(true); + } else { + if (!submitAccount) return; + try { + await updateAccount(account, { + cashout_address: submitAccount.cashout_address, + contact_data: submitAccount.contact_data, + }); + onUpdateSuccess(); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Forbidden + ? i18n.str`The rights to change the account are not sufficient` + : status === HttpStatusCode.NotFound + ? i18n.str`The username was not found` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + } + }} + /> +
    +
    +
    +

    +
    +
    + ); + } + \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx new file mode 100644 index 000000000..084a5b643 --- /dev/null +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -0,0 +1,131 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { VNode,h ,Fragment} from "preact"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; + +export function UpdateAccountPassword({ + account, + onClear, + onUpdateSuccess, + onLoadNotOk, + }: { + onLoadNotOk: ( + error: HttpResponsePaginated, + ) => VNode; + onClear: () => void; + onUpdateSuccess: () => void; + account: string; + }): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + const { changePassword } = useAdminAccountAPI(); + const [password, setPassword] = useState(); + const [repeat, setRepeat] = useState(); + + if (!result.ok) { + if (result.loading || result.type === ErrorType.TIMEOUT) { + return onLoadNotOk(result); + } + if (result.status === HttpStatusCode.NotFound) { + return
    account not found
    ; + } + return onLoadNotOk(result); + } + + const errors = undefinedIfEmpty({ + password: !password ? i18n.str`required` : undefined, + repeat: !repeat + ? i18n.str`required` + : password !== repeat + ? i18n.str`password doesn't match` + : undefined, + }); + + return ( +
    +
    +

    + Update password for {account} +

    +
    + +
    +
    +
    + + { + setPassword(e.currentTarget.value); + }} + /> + +
    +
    + + { + setRepeat(e.currentTarget.value); + }} + /> + +
    +
    +

    +

    +
    + { + e.preventDefault(); + onClear(); + }} + /> +
    +
    + { + e.preventDefault(); + if (!!errors || !password) return; + try { + const r = await changePassword(account, { + new_password: password, + }); + onUpdateSuccess(); + } catch (error) { + if (error instanceof RequestError) { + notify(buildRequestErrorMessage(i18n, error.cause)); + } else { + notifyError(i18n.str`Operation failed, please report`, (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString) + } + } + }} + /> +
    +
    +

    +
    +
    + ); + } \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index ced152feb..30fcbdff7 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -317,7 +317,8 @@ export function WithdrawalConfirmationQuestion({
    Amount
    -
    {Amounts.stringifyValue(details.amount)}
    +
    To be added
    + {/* Amounts.stringifyValue(details.amount) */}
    diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index b48e3b1dc..2a3a1ec2c 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -100,10 +100,6 @@ export function WithdrawalQRCode({ } if (data.confirmation_done) { - if (!settings.showWithdrawalSuccess) { - clearCurrentWithdrawal() - onContinue() - } return
    diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx new file mode 100644 index 000000000..8ab3e1323 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -0,0 +1,56 @@ +import { Amounts } from "@gnu-taler/taler-util"; +import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { useAccountDetails } from "../../hooks/access.js"; +import { useBackendContext } from "../../context/backend.js"; +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; + +export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { + const { i18n } = useTranslationContext(); + const r = useBackendContext(); + const account = r.state.status === "loggedIn" ? r.state.username : "admin"; + const result = useAccountDetails(account); + + if (!result.ok) { + return handleNotOkResult(i18n, onRegister)(result); + } + const { data } = result; + const balance = Amounts.parseOrThrow(data.balance.amount); + const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); + const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; + const limit = balanceIsDebit + ? Amounts.sub(debitThreshold, balance).amount + : Amounts.add(balance, debitThreshold).amount; + if (!balance) return ; + return ( + +
    +
    +

    {i18n.str`Bank account balance`}

    + {!balance ? ( +
    + Waiting server response... +
    + ) : ( +
    + {balanceIsDebit ? - : null} + {`${Amounts.stringifyValue(balance)}`} +   + {`${balance.currency}`} +
    + )} +
    +
    + { + notifyInfo(i18n.str`Wire transfer created!`); + }} + onCancel={undefined} + /> +
    + ); + } + \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx new file mode 100644 index 000000000..9ca0323a1 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -0,0 +1,219 @@ +import { VNode,h } from "preact"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; +import { useState } from "preact/hooks"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { parsePaytoUri } from "@gnu-taler/taler-util"; + +const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; +const EMAIL_REGEX = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; + +/** + * Create valid account object to update or create + * Take template as initial values for the form + * Purpose indicate if all field al read only (show), part of them (update) + * or none (create) + * @param param0 + * @returns + */ +export function AccountForm({ + template, + purpose, + onChange, + }: { + template: SandboxBackend.Circuit.CircuitAccountData | undefined; + onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; + purpose: "create" | "update" | "show"; + }): VNode { + const initial = initializeFromTemplate(template); + const [form, setForm] = useState(initial); + const [errors, setErrors] = useState< + RecursivePartial | undefined + >(undefined); + const { i18n } = useTranslationContext(); + + function updateForm(newForm: typeof initial): void { + const parsed = !newForm.cashout_address + ? undefined + : parsePaytoUri(newForm.cashout_address); + + const errors = undefinedIfEmpty>({ + cashout_address: !newForm.cashout_address + ? i18n.str`required` + : !parsed + ? i18n.str`does not follow the pattern` + : !parsed.isKnown || parsed.targetType !== "iban" + ? i18n.str`only "IBAN" target are supported` + : !IBAN_REGEX.test(parsed.iban) + ? i18n.str`IBAN should have just uppercased letters and numbers` + : validateIBAN(parsed.iban, i18n), + contact_data: undefinedIfEmpty({ + email: !newForm.contact_data?.email + ? i18n.str`required` + : !EMAIL_REGEX.test(newForm.contact_data.email) + ? i18n.str`it should be an email` + : undefined, + phone: !newForm.contact_data?.phone + ? i18n.str`required` + : !newForm.contact_data.phone.startsWith("+") + ? i18n.str`should start with +` + : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) + ? i18n.str`phone number can't have other than numbers` + : undefined, + }), + iban: !newForm.iban + ? undefined //optional field + : !IBAN_REGEX.test(newForm.iban) + ? i18n.str`IBAN should have just uppercased letters and numbers` + : validateIBAN(newForm.iban, i18n), + name: !newForm.name ? i18n.str`required` : undefined, + username: !newForm.username ? i18n.str`required` : undefined, + }); + setErrors(errors); + setForm(newForm); + onChange(errors === undefined ? (newForm as any) : undefined); + } + + return ( +
    +
    + + { + form.username = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + />{" "} + +
    +
    + + { + form.name = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    + {purpose !== "create" && ( +
    + + { + form.iban = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    + )} +
    + + { + form.contact_data.email = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    +
    + + { + form.contact_data.phone = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    +
    + + { + form.cashout_address = "payto://iban/" + e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    +
    + ); + } + + function initializeFromTemplate( + account: SandboxBackend.Circuit.CircuitAccountData | undefined, + ): WithIntermediate { + const emptyAccount = { + cashout_address: undefined, + iban: undefined, + name: undefined, + username: undefined, + contact_data: undefined, + }; + const emptyContact = { + email: undefined, + phone: undefined, + }; + + const initial: PartialButDefined = + structuredClone(account) ?? emptyAccount; + if (typeof initial.contact_data === "undefined") { + initial.contact_data = emptyContact; + } + initial.contact_data.email; + return initial as any; + } + + + \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx new file mode 100644 index 000000000..56b15818b --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -0,0 +1,120 @@ +import { h, VNode } from "preact"; +import { useBusinessAccounts } from "../../hooks/circuit.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { AccountAction } from "./Home.js"; +import { Amounts } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; + +interface Props { + onAction: (type: AccountAction, account: string) => void; + account: string | undefined; + onRegister: () => void; + +} + +export function AccountList({ account, onAction, onRegister }: Props): VNode { + const result = useBusinessAccounts({ account }); + const { i18n } = useTranslationContext(); + + if (result.loading) return
    ; + if (!result.ok) { + return handleNotOkResult(i18n, onRegister)(result); + } + + const { customers } = result.data; + return
    + {!customers.length ? ( +
    + ) : ( +
    +

    {i18n.str`Accounts:`}

    +
    + + + + + + + + + + + {customers.map((item, idx) => { + const balance = !item.balance + ? undefined + : Amounts.parse(item.balance.amount); + const balanceIsDebit = + item.balance && + item.balance.credit_debit_indicator == "debit"; + return ( + + + + + + + ); + })} + +
    {i18n.str`Username`}{i18n.str`Name`}{i18n.str`Balance`}{i18n.str`Actions`}
    + { + e.preventDefault(); + onAction("show-details", item.username) + }} + > + {item.username} + + {item.name} + {!balance ? ( + i18n.str`unknown` + ) : ( + + {balanceIsDebit ? - : null} + {`${Amounts.stringifyValue( + balance, + )}`} +   + {`${balance.currency}`} + + )} + + { + e.preventDefault(); + onAction("update-password", item.username) + }} + > + change password + +   + { + e.preventDefault(); + onAction("show-cashout", item.username) + }} + > + cashouts + +   + { + e.preventDefault(); + onAction("remove-account", item.username) + }} + > + remove + +
    +
    +
    + )} +
    +} \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx new file mode 100644 index 000000000..90835d52b --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -0,0 +1,107 @@ +import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h, Fragment } from "preact"; +import { useAdminAccountAPI } from "../../hooks/circuit.js"; +import { useState } from "preact/hooks"; +import { buildRequestErrorMessage } from "../../utils.js"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { getRandomPassword } from "../rnd.js"; +import { AccountForm } from "./AccountForm.js"; + +export function CreateNewAccount({ + onClose, + onCreateSuccess, +}: { + onClose: () => void; + onCreateSuccess: (password: string) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const { createAccount } = useAdminAccountAPI(); + const [submitAccount, setSubmitAccount] = useState< + SandboxBackend.Circuit.CircuitAccountData | undefined + >(); + return ( +
    +
    +

    + New account +

    +
    + +
    + { + setSubmitAccount(a); + }} + /> + +

    +

    +
    + { + e.preventDefault(); + onClose(); + }} + /> +
    +
    + { + e.preventDefault(); + + if (!submitAccount) return; + try { + const account: SandboxBackend.Circuit.CircuitAccountRequest = + { + cashout_address: submitAccount.cashout_address, + contact_data: submitAccount.contact_data, + internal_iban: submitAccount.iban, + name: submitAccount.name, + username: submitAccount.username, + password: getRandomPassword(), + }; + + await createAccount(account); + onCreateSuccess(account.password); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Forbidden + ? i18n.str`The rights to perform the operation are not sufficient` + : status === HttpStatusCode.BadRequest + ? i18n.str`Input data was invalid` + : status === HttpStatusCode.Conflict + ? i18n.str`At least one registration detail was not available` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + }} + /> +
    +
    +

    +
    +
    + ); +} diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx new file mode 100644 index 000000000..e1ec6cfe0 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/Home.tsx @@ -0,0 +1,162 @@ +import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { ShowCashoutDetails } from "../business/Home.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { ShowAccountDetails } from "../ShowAccountDetails.js"; +import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; +import { AdminAccount } from "./Account.js"; +import { AccountList } from "./AccountList.js"; +import { CreateNewAccount } from "./CreateNewAccount.js"; +import { RemoveAccount } from "./RemoveAccount.js"; + +/** + * Query account information and show QR code if there is pending withdrawal + */ +interface Props { + onRegister: () => void; +} +export type AccountAction = "show-details" | + "show-cashout" | + "update-password" | + "remove-account" | + "show-cashouts-details"; + +export function AdminHome({ onRegister }: Props): VNode { + const [action, setAction] = useState<{ + type: AccountAction, + account: string + }>() + + const [createAccount, setCreateAccount] = useState(false); + + const { i18n } = useTranslationContext(); + + if (action) { + switch (action.type) { + case "show-details": return { + setAction(undefined); + }} + /> + case "show-cashout": return ( +
    +
    +

    + Cashout for account {action.account} +

    +
    + { + setAction({ + type: "show-cashouts-details", + account: action.account + }); + }} + /> +

    + { + e.preventDefault(); + setAction(undefined); + }} + /> +

    +
    + ) + case "update-password": return { + notifyInfo(i18n.str`Password changed`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + case "remove-account": return { + notifyInfo(i18n.str`Account removed`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + case "show-cashouts-details": return { + setAction({ + type: "update-password", + account: action.account, + }) + }} + onUpdateSuccess={() => { + notifyInfo(i18n.str`Account updated`); + setAction(undefined); + }} + onClear={() => { + setAction(undefined); + }} + /> + } + } + + if (createAccount) { + return ( + setCreateAccount(false)} + onCreateSuccess={(password) => { + notifyInfo( + i18n.str`Account created with password "${password}". The user must change the password on the next login.`, + ); + setCreateAccount(false); + }} + /> + ); + } + + return ( + +
    +

    + Admin panel +

    +
    + +

    +

    +
    +
    + { + e.preventDefault(); + + setCreateAccount(true); + }} + /> +
    +
    +

    + + + + setAction({account, type})} onRegister={onRegister}/> + +
    + ); +} \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx new file mode 100644 index 000000000..2900db9d2 --- /dev/null +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -0,0 +1,112 @@ +import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode,h,Fragment } from "preact"; +import { useAccountDetails } from "../../hooks/access.js"; +import { useAdminAccountAPI } from "../../hooks/circuit.js"; +import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { buildRequestErrorMessage } from "../../utils.js"; + +export function RemoveAccount({ + account, + onClear, + onUpdateSuccess, + onLoadNotOk, + }: { + onLoadNotOk: ( + error: HttpResponsePaginated, + ) => VNode; + onClear: () => void; + onUpdateSuccess: () => void; + account: string; + }): VNode { + const { i18n } = useTranslationContext(); + const result = useAccountDetails(account); + const { deleteAccount } = useAdminAccountAPI(); + + if (!result.ok) { + if (result.loading || result.type === ErrorType.TIMEOUT) { + return onLoadNotOk(result); + } + if (result.status === HttpStatusCode.NotFound) { + return
    account not found
    ; + } + return onLoadNotOk(result); + } + + const balance = Amounts.parse(result.data.balance.amount); + if (!balance) { + return
    there was an error reading the balance
    ; + } + const isBalanceEmpty = Amounts.isZero(balance); + return ( +
    +
    +

    + Remove account: {account} +

    +
    + {/* {FXME: SHOW WARNING} */} + {/* {!isBalanceEmpty && ( + saveError(undefined)} + /> + )} */} + +

    +

    +
    + { + e.preventDefault(); + onClear(); + }} + /> +
    +
    + { + e.preventDefault(); + try { + const r = await deleteAccount(account); + onUpdateSuccess(); + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Forbidden + ? i18n.str`The administrator specified a institutional username` + : status === HttpStatusCode.NotFound + ? i18n.str`The username was not found` + : status === HttpStatusCode.PreconditionFailed + ? i18n.str`Balance was not zero` + : undefined, + }), + ); + } else { + notifyError(i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString); + } + } + }} + /> +
    +
    +

    +
    + ); + } + \ No newline at end of file diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx new file mode 100644 index 000000000..8beea640a --- /dev/null +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -0,0 +1,757 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + AmountJson, + Amounts, + HttpStatusCode, + TranslatedString +} from "@gnu-taler/taler-util"; +import { + HttpResponse, + HttpResponsePaginated, + RequestError, + notify, + notifyError, + notifyInfo, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useAccountDetails } from "../../hooks/access.js"; +import { + useCashoutDetails, + useCircuitAccountAPI, + useEstimator, + useRatiosAndFeeConfig, +} from "../../hooks/circuit.js"; +import { + TanChannel, + buildRequestErrorMessage, + undefinedIfEmpty, +} from "../../utils.js"; +import { handleNotOkResult } from "../HomePage.js"; +import { LoginForm } from "../LoginForm.js"; +import { Amount } from "../PaytoWireTransferForm.js"; +import { ShowAccountDetails } from "../ShowAccountDetails.js"; +import { UpdateAccountPassword } from "../UpdateAccountPassword.js"; + +interface Props { + account: string, + onClose: () => void; + onRegister: () => void; + onLoadNotOk: () => void; +} +export function BusinessAccount({ + onClose, + account, + onLoadNotOk, + onRegister, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const [updatePassword, setUpdatePassword] = useState(false); + const [newCashout, setNewcashout] = useState(false); + const [showCashoutDetails, setShowCashoutDetails] = useState< + string | undefined + >(); + + + if (newCashout) { + return ( + { + setNewcashout(false); + }} + onComplete={(id) => { + notifyInfo( + i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`, + ); + setNewcashout(false); + setShowCashoutDetails(id); + }} + /> + ); + } + if (showCashoutDetails) { + return ( + { + setShowCashoutDetails(undefined); + }} + /> + ); + } + if (updatePassword) { + return ( + { + notifyInfo(i18n.str`Password changed`); + setUpdatePassword(false); + }} + onClear={() => { + setUpdatePassword(false); + }} + /> + ); + } + return ( +
    + { + notifyInfo(i18n.str`Account updated`); + }} + onChangePassword={() => { + setUpdatePassword(true); + }} + onClear={onClose} + /> +
    +
    +

    {i18n.str`Latest cashouts`}

    + { + setShowCashoutDetails(id); + }} + /> +
    +
    +
    +
    + { + e.preventDefault(); + setNewcashout(true); + }} + /> +
    +
    +
    + ); +} + +interface PropsCashout { + account: string; + onComplete: (id: string) => void; + onCancel: () => void; + onLoadNotOk: ( + error: + | HttpResponsePaginated + | HttpResponse, + ) => VNode; +} + +type FormType = { + isDebit: boolean; + amount: string; + subject: string; + channel: TanChannel; +}; +type ErrorFrom = { + [P in keyof T]+?: string; +}; + +// check #7719 +function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse< + SandboxBackend.Circuit.Config & { hasChanged?: boolean }, + SandboxBackend.SandboxError +> { + const result = useRatiosAndFeeConfig(); + const [oldResult, setOldResult] = useState< + SandboxBackend.Circuit.Config | undefined + >(undefined); + const dataFromBackend = result.ok ? result.data : undefined; + useEffect(() => { + // save only the first result of /config to the backend + if (!dataFromBackend || oldResult !== undefined) return; + setOldResult(dataFromBackend); + }, [dataFromBackend]); + + if (!result.ok) return result; + + const data = !oldResult ? result.data : oldResult; + const hasChanged = + oldResult && + (result.data.name !== oldResult.name || + result.data.version !== oldResult.version || + result.data.ratios_and_fees.buy_at_ratio !== + oldResult.ratios_and_fees.buy_at_ratio || + result.data.ratios_and_fees.buy_in_fee !== + oldResult.ratios_and_fees.buy_in_fee || + result.data.ratios_and_fees.sell_at_ratio !== + oldResult.ratios_and_fees.sell_at_ratio || + result.data.ratios_and_fees.sell_out_fee !== + oldResult.ratios_and_fees.sell_out_fee || + result.data.fiat_currency !== oldResult.fiat_currency); + + return { + ...result, + data: { ...data, hasChanged }, + }; +} + +function CreateCashout({ + account, + onComplete, + onCancel, + onLoadNotOk, +}: PropsCashout): VNode { + const { i18n } = useTranslationContext(); + const ratiosResult = useRatiosAndFeeConfig(); + const result = useAccountDetails(account); + const { + estimateByCredit: calculateFromCredit, + estimateByDebit: calculateFromDebit, + } = useEstimator(); + const [form, setForm] = useState>({ isDebit: true }); + + const { createCashout } = useCircuitAccountAPI(); + if (!result.ok) return onLoadNotOk(result); + if (!ratiosResult.ok) return onLoadNotOk(ratiosResult); + const config = ratiosResult.data; + + const balance = Amounts.parseOrThrow(result.data.balance.amount); + const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold); + const zero = Amounts.zeroOfCurrency(balance.currency); + const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit"; + const limit = balanceIsDebit + ? Amounts.sub(debitThreshold, balance).amount + : Amounts.add(balance, debitThreshold).amount; + + const zeroCalc = { debit: zero, credit: zero, beforeFee: zero }; + const [calc, setCalc] = useState(zeroCalc); + const sellRate = config.ratios_and_fees.sell_at_ratio; + const sellFee = !config.ratios_and_fees.sell_out_fee + ? zero + : Amounts.parseOrThrow( + `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`, + ); + const fiatCurrency = config.fiat_currency; + + if (!sellRate || sellRate < 0) return
    error rate
    ; + + const amount = Amounts.parseOrThrow( + `${!form.isDebit ? fiatCurrency : balance.currency}:${ + !form.amount ? "0" : form.amount + }`, + ); + + useEffect(() => { + if (form.isDebit) { + calculateFromDebit(amount, sellFee, sellRate) + .then((r) => { + setCalc(r); + }) + .catch((error) => { + notify( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + type: "error", + title: i18n.str`Could not estimate the cashout`, + description: error.message as TranslatedString + }, + ); + }); + } else { + calculateFromCredit(amount, sellFee, sellRate) + .then((r) => { + setCalc(r); + }) + .catch((error) => { + notify( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + type: "error", + title: i18n.str`Could not estimate the cashout`, + description: error.message, + }, + ); + }); + } + }, [form.amount, form.isDebit]); + + const balanceAfter = Amounts.sub(balance, calc.debit).amount; + + function updateForm(newForm: typeof form): void { + setForm(newForm); + } + const errors = undefinedIfEmpty>({ + amount: !form.amount + ? i18n.str`required` + : !amount + ? i18n.str`could not be parsed` + : Amounts.cmp(limit, calc.debit) === -1 + ? i18n.str`balance is not enough` + : Amounts.cmp(calc.beforeFee, sellFee) === -1 + ? i18n.str`the total amount to transfer does not cover the fees` + : Amounts.isZero(calc.credit) + ? i18n.str`the total transfer at destination will be zero` + : undefined, + channel: !form.channel ? i18n.str`required` : undefined, + }); + + return ( +
    +

    New cashout

    +
    +
    + + { + form.subject = e.currentTarget.value; + updateForm(structuredClone(form)); + }} + /> + +
    +
    + +
    + { + form.amount = v; + updateForm(structuredClone(form)); + }} + error={errors?.amount} + /> + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    {" "} + {Amounts.isZero(sellFee) ? undefined : ( + +
    + + +
    + +
    + + +
    +
    + )} +
    + + +
    +
    + + +
    + { + e.preventDefault(); + form.channel = TanChannel.EMAIL; + updateForm(structuredClone(form)); + }} + /> + { + e.preventDefault(); + form.channel = TanChannel.SMS; + updateForm(structuredClone(form)); + }} + /> + { + e.preventDefault(); + form.channel = TanChannel.FILE; + updateForm(structuredClone(form)); + }} + /> +
    + +
    +
    +
    + + + +
    +
    +
    + ); +} + +interface ShowCashoutProps { + id: string; + onCancel: () => void; + onLoadNotOk: ( + error: HttpResponsePaginated, + ) => VNode; +} +export function ShowCashoutDetails({ + id, + onCancel, + onLoadNotOk, +}: ShowCashoutProps): VNode { + const { i18n } = useTranslationContext(); + const result = useCashoutDetails(id); + const { abortCashout, confirmCashout } = useCircuitAccountAPI(); + const [code, setCode] = useState(undefined); + if (!result.ok) return onLoadNotOk(result); + const errors = undefinedIfEmpty({ + code: !code ? i18n.str`required` : undefined, + }); + const isPending = String(result.data.status).toUpperCase() === "PENDING"; + return ( +
    +

    Cashout details {id}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + {isPending ? ( +
    + + { + setCode(e.currentTarget.value); + }} + /> + +
    + ) : undefined} +
    +
    +
    + + {isPending ? ( +
    + +   + +
    + ) : ( +
    + )} +
    +
    + ); +} + +const MAX_AMOUNT_DIGIT = 2; +/** + * Truncate the amount of digits to display + * in the form based on the fee calculations + * + * Backend must have the same truncation + * @param a + * @returns + */ +function truncate(a: AmountJson): AmountJson { + const str = Amounts.stringify(a); + const idx = str.indexOf("."); + if (idx === -1) { + return a; + } + const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT); + return Amounts.parseOrThrow(truncated); +} + +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} -- cgit v1.2.3 From 0b7bbed99d155ba030a1328e357ab6751bdbb835 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 13:10:16 -0300 Subject: more ui: business and admin --- packages/demobank-ui/src/components/Routing.tsx | 17 +- .../src/components/ShowInputErrorLabel.tsx | 4 +- .../demobank-ui/src/pages/AccountPage/index.ts | 5 +- .../demobank-ui/src/pages/AccountPage/state.ts | 7 +- .../demobank-ui/src/pages/AccountPage/views.tsx | 29 +- packages/demobank-ui/src/pages/BankFrame.tsx | 32 +- packages/demobank-ui/src/pages/HomePage.tsx | 6 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 131 +++--- .../src/pages/PaytoWireTransferForm.tsx | 6 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 2 +- .../demobank-ui/src/pages/ShowAccountDetails.tsx | 278 ++++++------ .../src/pages/UpdateAccountPassword.tsx | 278 +++++++----- packages/demobank-ui/src/pages/admin/Account.tsx | 72 ++- .../demobank-ui/src/pages/admin/AccountForm.tsx | 494 +++++++++++++-------- .../demobank-ui/src/pages/admin/AccountList.tsx | 182 +++++--- .../src/pages/admin/CreateNewAccount.tsx | 174 ++++---- packages/demobank-ui/src/pages/admin/Home.tsx | 51 +-- .../demobank-ui/src/pages/admin/RemoveAccount.tsx | 302 +++++++++---- packages/demobank-ui/src/pages/business/Home.tsx | 2 +- 19 files changed, 1181 insertions(+), 891 deletions(-) (limited to 'packages/demobank-ui/src/pages/HomePage.tsx') diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index ef11af76e..b8e39948b 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -33,12 +33,7 @@ export function Routing(): VNode { const backend = useBackendContext(); if (backend.state.status === "loggedOut") { - return { - route("/business"); - }} - > + return { - route("/business"); - }} - > + { route(`/operation/${wopid}`); }} + goToBusinessAccount={() => { + route("/business"); + }} onRegister={() => { route("/register"); }} diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx index dacffe20a..c5840cad9 100644 --- a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx +++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx @@ -24,6 +24,6 @@ export function ShowInputErrorLabel({ isDirty: boolean; }): VNode { if (message && isDirty) - return
    {message}
    ; - return ; + return
    {message}
    ; + return
    ; } diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index ed6945f84..128a6d30f 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -28,6 +28,7 @@ export interface Props { onLoadNotOk: ( error: HttpResponsePaginated, ) => VNode; + goToBusinessAccount: () => void; } export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound; @@ -51,10 +52,8 @@ export namespace State { status: "ready"; error: undefined; account: string, - payto: PaytoUriIBAN | PaytoUriTalerBank, - balance: AmountJson, - balanceIsDebit: boolean, limit: AmountJson, + goToBusinessAccount: () => void; } export interface InvalidIban { diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index 2249b743e..a57e19901 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAccountDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; -export function useComponentState({ account, onLoadNotOk }: Props): State { +export function useComponentState({ account, goToBusinessAccount }: Props): State { const result = useAccountDetails(account); const backend = useBackendContext(); const { i18n } = useTranslationContext(); @@ -60,7 +60,6 @@ export function useComponentState({ account, onLoadNotOk }: Props): State { const payto = parsePaytoUri(data.paytoUri); if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) { - console.log(payto) return { status: "invalid-iban", error: result @@ -75,11 +74,9 @@ export function useComponentState({ account, onLoadNotOk }: Props): State { return { status: "ready", + goToBusinessAccount, error: undefined, account, - balance, - balanceIsDebit, limit, - payto }; } diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index f2cbbba6c..abd14848f 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -22,6 +22,7 @@ import { PaymentOptions } from "../PaymentOptions.js"; import { State } from "./index.js"; import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; +import { useBusinessAccountDetails } from "../../hooks/circuit.js"; export function InvalidIbanView({ error }: State.InvalidIban) { return ( @@ -77,11 +78,35 @@ function ImportantMessage(): VNode { } -export function ReadyView({ account, balance, balanceIsDebit, limit, payto }: State.Ready): VNode<{}> { - const { i18n } = useTranslationContext(); +export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { return + ; } +function MaybeBusinessButton({ + account, + onClick, +}: { + account: string; + onClick: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + if (!result.ok) return ; + return ( +
    + +
    + ); +} diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 80a8224d4..4b23686d6 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -39,36 +39,12 @@ const versionText = VERSION const logger = new Logger("BankFrame"); -function MaybeBusinessButton({ - account, - onClick, -}: { - account: string; - onClick: () => void; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - if (!result.ok) return ; - return ( - { - e.preventDefault(); - onClick(); - }} - >{i18n.str`Business Profile`} - ); -} - export function BankFrame({ children, - goToBusinessAccount, account, }: { - account: string | undefined, + account?: string, children: ComponentChildren; - goToBusinessAccount?: () => void; }): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); @@ -489,5 +465,9 @@ function AccountBalance({ account }: { account: string }): VNode { const result = useAccountDetails(account); if (!result.ok) return
    - return
    {result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} {Amounts.currencyOf(result.data.balance.amount)} {Amounts.stringifyValue(result.data.balance.amount)}
    + return
    + {Amounts.currencyOf(result.data.balance.amount)} +  {result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} + {Amounts.stringifyValue(result.data.balance.amount)} +
    } diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index a911f347c..40cc147a6 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -31,14 +31,11 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -import { useBackendContext } from "../context/backend.js"; import { getInitialBackendBaseURL } from "../hooks/backend.js"; import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; -import { AdminHome } from "./admin/Home.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; -import { error } from "console"; const logger = new Logger("AccountPage"); @@ -56,10 +53,12 @@ export function HomePage({ onRegister, account, onPendingOperationFound, + goToBusinessAccount, }: { account: string, onPendingOperationFound: (id: string) => void; onRegister: () => void; + goToBusinessAccount: () => void; }): VNode { const [settings] = useSettings(); const { i18n } = useTranslationContext(); @@ -72,6 +71,7 @@ export function HomePage({ return ( ); diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index c82c1b28d..5cb09a5d4 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -33,76 +33,79 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); - return (
    - - Send money to - + return ( +
    + + Send money to + -
    - {/* */} - +
    + {tab === "charge-wallet" && ( + { + updateSettings("currentWithdrawalOperationId", id); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} + {tab === "wire-transfer" && ( + { + notifyInfo(i18n.str`Wire transfer created!`); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} -
    ) +
    + ) } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index cdaf363e3..af6f7caee 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -44,10 +44,12 @@ const logger = new Logger("PaytoWireTransferForm"); export function PaytoWireTransferForm({ focus, + title, onSuccess, onCancel, limit, }: { + title: TranslatedString, focus?: boolean; onSuccess: () => void; onCancel: (() => void) | undefined; @@ -158,7 +160,9 @@ export function PaytoWireTransferForm({ return (
    -

    Transfer details

    +

    + {title} +