diff options
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/index.ts | 4 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/state.ts | 10 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AdminPage.tsx | 150 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BankFrame.tsx | 409 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BusinessAccount.tsx | 106 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/HomePage.tsx | 40 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/LoginForm.tsx | 335 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaymentOptions.tsx | 48 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx | 221 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/QrCodeSection.tsx | 126 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/RegistrationPage.tsx | 311 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WalletWithdrawForm.tsx | 319 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 383 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 23 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/rnd.ts | 2890 |
15 files changed, 4349 insertions, 1026 deletions
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 <http://www.gnu.org/licenses/> */ -import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; import { 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} /> </Fragment> ); @@ -414,7 +416,6 @@ export function UpdateAccountPassword({ const { changePassword } = useAdminAccountAPI(); const [password, setPassword] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>(); - const [error, saveError] = useState<ErrorMessage | undefined>(); 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({ <i18n.Translate>Update password for {account}</i18n.Translate> </h1> </div> - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> <form class="pure-form"> @@ -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<ErrorMessage | undefined>(); return ( <div> <div> @@ -548,9 +541,6 @@ function CreateNewAccount({ <i18n.Translate>New account</i18n.Translate> </h1> </div> - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> <AccountForm @@ -587,39 +577,38 @@ function CreateNewAccount({ 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(), - }; + { + 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) { - saveError( + 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, + ? 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<ErrorMessage | undefined>(); if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { @@ -673,9 +661,6 @@ export function ShowAccountDetails({ <i18n.Translate>Business account details</i18n.Translate> </h1> </div> - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> <AccountForm template={result.data} @@ -740,24 +725,23 @@ export function ShowAccountDetails({ onUpdateSuccess(); } catch (error) { if (error instanceof RequestError) { - saveError( + 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, + ? 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<ErrorMessage | undefined>(); if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { @@ -812,7 +795,8 @@ function RemoveAccount({ <i18n.Translate>Remove account: {account}</i18n.Translate> </h1> </div> - {!isBalanceEmpty && ( + {/* {FXME: SHOW WARNING} */} + {/* {!isBalanceEmpty && ( <ErrorBannerFloat error={{ title: i18n.str`Can't delete the account`, @@ -820,10 +804,7 @@ function RemoveAccount({ }} onClear={() => saveError(undefined)} /> - )} - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} + )} */} <p> <div style={{ display: "flex", justifyContent: "space-between" }}> @@ -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({ </a>, ); - return (<div class="min-h-full"> + return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;"> <div class="bg-indigo-600 pb-32"> - <nav class="border-b border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-none"> + <nav class=""> <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8"> - <div class="relative flex h-16 items-center justify-between lg:border-b lg:border-indigo-400 lg:border-opacity-25"> + <div class="relative flex h-16 items-center justify-between "> <div class="flex items-center px-2 lg:px-0"> - <div class="flex-shrink-0"> - <img class="block h-8 w-8" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=300" alt="Your Company" /> + <div class="flex-shrink-0 bg-white rounded-lg"> + <a href="#/"> + <img + class="h-8 w-auto" + src={logo} + alt="Taler" + style={{ height: 35, margin: 10 }} + /> + </a> </div> - <div class="hidden lg:ml-10 lg:block"> + <div class="hidden sm:block lg:ml-10 "> <div class="flex space-x-4"> {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */} {bankUiSettings.demoSites.map(([name, url]) => { @@ -100,62 +106,131 @@ export function BankFrame({ </div> </div> - <div class="flex lg:hidden"> - {/* <!-- Mobile menu button --> */} - <button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false" + <div class="flex"> + <button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false" onClick={(e) => { setOpen(!open) }}> <span class="absolute -inset-0.5"></span> <span class="sr-only">Open main menu</span> - {/* <!-- Menu open: "hidden", Menu closed: "block" --> */} - <svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> </svg> - {/* <!-- Menu open: "block", Menu closed: "hidden" --> */} - <svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> - </svg> </button> </div> </div> </div> - {/* <!-- Mobile menu, show/hide based on menu state. --> */} {open && - <div class="lg:hidden" id="mobile-menu"> - <div class="space-y-1 px-2 pb-3 pt-2"> - {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */} - {bankUiSettings.demoSites.map(([name, url]) => { - return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md py-2 px-3 text-base font-medium">{name}</a> - })} + <Fragment> + <div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true" + onClick={() => { + setOpen(false) + }}> + <div class="fixed inset-0"></div> + + <div class="fixed inset-0 overflow-hidden"> + <div class="absolute inset-0 overflow-hidden"> + <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10"> + <div class="pointer-events-auto w-screen max-w-md" > + <div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => { + //do not trigger close if clicking inside the sidebar + e.stopPropagation(); + }}> + <div class="px-4 sm:px-6" > + <div class="flex items-start justify-between" > + <h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title"> + <i18n.Translate>Settings</i18n.Translate> + </h2> + <div class="ml-3 flex h-7 items-center"> + <button type="button" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + onClick={(e) => { + setOpen(false) + }} + + > + <span class="absolute -inset-2.5"></span> + <span class="sr-only"> + <i18n.Translate>Close panel</i18n.Translate> + </span> + <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + </div> + </div> + <div class="relative mt-6 flex-1 px-4 sm:px-6"> + {/* <!-- Your content --> */} + + <nav class="flex flex-1 flex-col" aria-label="Sidebar"> + <ul role="list" class="flex flex-1 flex-col gap-y-7"> + <li> + <ul role="list" class="-mx-2 space-y-1"> + <li> + <a href="#" + class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold" + onClick={() => { + backend.logOut(); + setOpen(false) + updateSettings("currentWithdrawalOperationId", undefined); + }} + > + <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> + </svg> + Log out + {/* <span class="ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-gray-50 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600 ring-1 ring-inset ring-gray-200" aria-hidden="true">5</span> */} + </a> + </li> + </ul> + </li> + <li class="sm:hidden"> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Sites</i18n.Translate> + </div> + <ul role="list" class="-mx-2 mt-2 space-y-1"> + {bankUiSettings.demoSites.map(([name, url]) => { + return <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"> + <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">></span> + <span class="truncate">{name}</span> + </a> + })} + </ul> + </li> + </ul> + </nav> + </div> + </div> + </div> + </div> + </div> + </div> </div> - </div> + </Fragment> } </nav > - <header class="py-10"> - - <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> - <div class=" flex flex-wrap items-center justify-between sm:flex-nowrap"> - {/* <h1 class="text-base font-semibold leading-6 text-gray-900"></h1> */} - <h1 class="text-3xl font-bold tracking-tight text-white"><WelcomeAccount /></h1> - <div> - <h2 class="text-3xl font-bold tracking-tight text-white">KUDOS 100.00</h2> + {true && + <header class="py-5 border-t border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-t lg:border-indigo-400 lg:border-opacity-25"> + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> + <div class=" flex flex-wrap items-center justify-between sm:flex-nowrap"> + <h3 class="text-2xl font-bold tracking-tight text-white"><WelcomeAccount /></h3> + <div> + <h3 class="text-2xl font-bold tracking-tight text-white"><AccountBalance /></h3> + </div> </div> - {/* <div class="ml-4 mt-2 flex-shrink-0"> - <button type="button" class="relative inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Create new job</button> - </div> */} </div> - </div> - </header> + + </header> + } </div > - <main class="-mt-32"> + <main class="-mt-32 flex-1"> <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8"> <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6"> - {/* <!-- Your content --> */} + <StatusBanner /> {children} </div> </div> @@ -207,15 +282,15 @@ export function BankFrame({ // /> // ) : undefined} - // <LangSelector /> + // <LangSelector /> // <a // href="#" // class="pure-button logout-button" - // onClick={() => { - // backend.logOut(); - // updateSettings("currentWithdrawalOperationId", undefined); - // }} + // onClick={() => { + // backend.logOut(); + // updateSettings("currentWithdrawalOperationId", undefined); + // }} // >{i18n.str`Logout`}</a> // </Fragment> // ) : undefined} @@ -225,149 +300,110 @@ export function BankFrame({ // <StatusBanner /> // {children} // </section> - // <section id="footer" class="footer"> - // <hr /> - // <div> - // <p> - // You can learn more about GNU Taler on our{" "} - // <a href="https://taler.net">main website</a>. - // </p> - // </div> - // <div style="flex-grow:1" /> - // <p> - // Copyright © 2014—2022 Taler Systems SA. {versionText}{" "} - // <TestingTag /> - // </p> - // </section> // </Fragment> ); } -function maybeDemoContent(content: VNode): VNode { - if (bankUiSettings.showDemoNav) { - return content; - } - return <Fragment />; -} +// function maybeDemoContent(content: VNode): VNode { +// if (bankUiSettings.showDemoNav) { +// return content; +// } +// return <Fragment />; +// } -export function ErrorBannerFloat({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( - <div - style={{ - position: "fixed", - top: 10, - zIndex: 200, - width: "90%", - }} - > - <ErrorBanner error={error} onClear={onClear} /> - </div> - ); -} +// export function ErrorBannerFloat({ +// error, +// onClear, +// }: { +// error: ErrorMessage; +// onClear?: () => void; +// }): VNode { +// return ( +// <div +// style={{ +// position: "fixed", +// top: 10, +// zIndex: 200, +// width: "90%", +// }} +// > +// <ErrorBanner error={error} onClear={onClear} /> +// </div> +// ); +// } -function ErrorBanner({ - error, - onClear, -}: { - error: ErrorMessage; - onClear?: () => void; -}): VNode { - return ( - <div - class="informational informational-fail" - style={{ - marginTop: 8, - paddingLeft: 16, - paddingRight: 16, - }} - > - <div style={{ display: "flex", justifyContent: "space-between" }}> - <p> - <b>{error.title}</b> - </p> - <div style={{ marginTop: "auto", marginBottom: "auto" }}> - {onClear && ( - <input - type="button" - class="pure-button" - value="Clear" - onClick={(e) => { - e.preventDefault(); - onClear(); - }} - /> - )} - </div> - </div> - <p>{error.description}</p> - </div> - ); -} - -function StatusBanner(): VNode | null { - const [info, setInfo] = useState<TranslatedString>(); - const [error, setError] = useState<ErrorMessage>(); - useEffect(() => { - return onNotificationUpdate((newValue) => { - if (newValue === undefined) { - setInfo(undefined); - setError(undefined); - } else { - if (newValue.type === "error") { - setError(newValue.error); - } else { - setInfo(newValue.info); - } - } - }); - }, []); - return ( - <div - style={{ - position: "fixed", - top: 10, - zIndex: 200, - width: "90%", - }} - > - {!info ? undefined : ( - <div - class="informational informational-ok" - style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }} - > - <div style={{ display: "flex", justifyContent: "space-between" }}> - <p> - <b>{info}</b> - </p> - <div> - <input - type="button" - class="pure-button" - value="Clear" - onClick={async () => { - setInfo(undefined); - }} - /> +function StatusBanner(): VNode { + const notifs = useNotifications() + return <div + class="fixed top-10 z-20 ml-4 mr-4" + > { + 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 <div class="rounded-md bg-red-50 p-4"> + <div class="flex"> + <div class="flex-shrink-0"> + <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /> + </svg> + </div> + <div class="ml-3 flex-1 md:flex md:justify-between"> + <p class="text-sm font-medium text-red-800">{n.message.title}</p> + <p class="mt-3 text-sm md:ml-6 md:mt-0"> + <button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + onClick={(e) => { + e.preventDefault(); + n.remove() + }} + > + Close + <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> + </svg> + </button> + </p> + </div> </div> + {n.message.description && + <div class="mt-2 text-sm text-red-700"> + {n.message.description} + </div> + } </div> - </div> - )} - {!error ? undefined : ( - <ErrorBanner - error={error} - onClear={() => { - setError(undefined); - }} - /> - )} - </div> - ); + case "info": + return <div class="rounded-md bg-green-50 border-4 border-green-600 p-6"> + <div class="flex"> + <div class="flex-shrink-0"> + <svg class="h-8 w-8 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </div> + <div class="ml-3 flex-1 md:flex md:justify-between"> + <h3 class="text-lg font-medium text-green-800">{n.message.title}</h3> + + <p class="mt-3 text-sm md:ml-6 md:mt-0"> + <button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-md text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + onClick={(e) => { + e.preventDefault(); + n.remove(); + }} + > + Close + <svg class="h-8 w-8" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> + </svg> + </button> + </p> + </div> + + </div> + </div> + } + })} + </div> + } function TestingTag(): VNode { @@ -392,12 +428,12 @@ function TestingTag(): VNode { function Footer() { return ( - <footer class="absolute bottom-4"> + <footer class="bottom-4 mb-4"> <div class="mt-8 mx-8 md:order-1 md:mt-0"> <div> <p class="text-xs leading-5 text-gray-400"> You can learn more about GNU Taler on our{" "} - <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">main website</a>. + <a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">main website</a>. </p> </div> <div style="flex-grow:1" /> @@ -418,4 +454,9 @@ function WelcomeAccount(): VNode { Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} /> </i18n.Translate> -}
\ No newline at end of file +} + +function AccountBalance(): VNode { + + return <div>KUDOS 100.00</div> +} 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<ErrorMessage | undefined>(); 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 ( <div> - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} <h1>New cashout</h1> <form class="pure-form"> <fieldset> @@ -341,13 +338,15 @@ function CreateCashout({ /> </fieldset> <fieldset> - <label> + <label for="amount"> {form.isDebit ? i18n.str`Amount to send` : i18n.str`Amount to receive`} + </label> <div style={{ display: "flex" }}> <Amount + name="amount" currency={amount.currency} value={form.amount} onChange={(v) => { @@ -376,24 +375,27 @@ function CreateCashout({ <input value={sellRate} disabled /> </fieldset> <fieldset> - <label>{i18n.str`Balance now`}</label> + <label for="balance-now">{i18n.str`Balance now`}</label> <Amount + name="banace-now" currency={balance.currency} value={Amounts.stringifyValue(balance)} /> </fieldset> <fieldset> - <label + <label for="total-cost" style={{ fontWeight: "bold", color: "red" }} >{i18n.str`Total cost`}</label> <Amount + name="total-cost" currency={balance.currency} value={Amounts.stringifyValue(calc.debit)} /> </fieldset> <fieldset> - <label>{i18n.str`Balance after`}</label> + <label for="balance-after">{i18n.str`Balance after`}</label> <Amount + name="balance-after" currency={balance.currency} value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""} /> @@ -401,16 +403,18 @@ function CreateCashout({ {Amounts.isZero(sellFee) ? undefined : ( <Fragment> <fieldset> - <label>{i18n.str`Amount after conversion`}</label> + <label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label> <Amount + name="amount-conversion" currency={fiatCurrency} value={Amounts.stringifyValue(calc.beforeFee)} /> </fieldset> <fieldset> - <label>{i18n.str`Cashout fee`}</label> + <label form="cashout-fee">{i18n.str`Cashout fee`}</label> <Amount + name="cashout-fee" currency={fiatCurrency} value={Amounts.stringifyValue(sellFee)} /> @@ -418,10 +422,11 @@ function CreateCashout({ </Fragment> )} <fieldset> - <label + <label for="total" style={{ fontWeight: "bold", color: "green" }} >{i18n.str`Total cashout transfer`}</label> <Amount + name="total" currency={fiatCurrency} value={Amounts.stringifyValue(calc.credit)} /> @@ -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<string | undefined>(undefined); - const [error, saveError] = useState<ErrorMessage | undefined>(); if (!result.ok) return onLoadNotOk(result); const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, @@ -574,9 +577,6 @@ export function ShowCashoutDetails({ return ( <div> <h1>Cashout details {id}</h1> - {error && ( - <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} <form class="pure-form"> <fieldset> <label> @@ -661,7 +661,7 @@ export function ShowCashoutDetails({ onCancel(); } catch (error) { if (error instanceof RequestError) { - saveError( + notify( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.NotFound @@ -672,14 +672,13 @@ export function ShowCashoutDetails({ }), ); } 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 + ) + } } }} > @@ -699,7 +698,7 @@ export function ShowCashoutDetails({ }); } catch (error) { if (error instanceof RequestError) { - saveError( + notify( buildRequestErrorMessage(i18n, error.cause, { onClientError: (status) => status === HttpStatusCode.NotFound @@ -714,14 +713,13 @@ export function ShowCashoutDetails({ }), ); } 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 + ) + } } }} > diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 86e511284..e00daf278 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -17,6 +17,7 @@ import { HttpStatusCode, Logger, + TranslatedString, parseWithdrawUri, stringifyWithdrawUri, } from "@gnu-taler/taler-util"; @@ -24,18 +25,20 @@ import { ErrorType, HttpResponse, HttpResponsePaginated, + notify, + notifyError, useTranslationContext, } 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 { notifyError, notifyInfo } from "../hooks/notification.js"; import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; import { AdminPage } from "./AdminPage.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; +import { error } from "console"; const logger = new Logger("AccountPage"); @@ -100,9 +103,10 @@ export function WithdrawalOperationPage({ const { i18n } = useTranslationContext(); if (!parsedUri) { - notifyError({ - title: i18n.str`The Withdrawal URI is not valid: "${uri}"`, - }); + notifyError( + i18n.str`The Withdrawal URI is not valid: "${uri}"`, + undefined + ); return <Loading />; } @@ -132,46 +136,46 @@ export function handleNotOkResult( if (!result.ok) { switch (result.type) { case ErrorType.TIMEOUT: { - notifyError({ - title: i18n.str`Request timeout, try again later.`, - }); + notifyError(i18n.str`Request timeout, try again later.`, undefined); break; } case ErrorType.CLIENT: { if (result.status === HttpStatusCode.Unauthorized) { - notifyError({ - title: i18n.str`Wrong credentials`, - }); + notifyError(i18n.str`Wrong credentials`, undefined); return <LoginForm onRegister={onRegister} />; } const errorData = result.payload; - notifyError({ + notify({ + type: "error", title: i18n.str`Could not load due to a client error`, - description: errorData.error.description, + description: errorData.error.description as TranslatedString, debug: JSON.stringify(result), }); break; } case ErrorType.SERVER: { - notifyError({ + notify({ + type: "error", title: i18n.str`Server returned with error`, - description: result.payload.error.description, + description: result.payload.error.description as TranslatedString, debug: JSON.stringify(result.payload), }); break; } case ErrorType.UNREADABLE: { - notifyError({ + notify({ + type:"error", title: i18n.str`Unexpected error.`, - description: `Response from ${result.info?.url} is unreadable, http status: ${result.status}`, + description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`, debug: JSON.stringify(result), }); break; } case ErrorType.UNEXPECTED: { - notifyError({ + notify({ + type:"error", title: i18n.str`Unexpected error.`, - description: `Diagnostic from ${result.info?.url} is "${result.message}"`, + description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`, debug: JSON.stringify(result), }); break; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index f0ae97d60..46039005a 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -14,16 +14,14 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; +import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; import { useCredentialsChecker } from "../hooks/backend.js"; -import { ErrorMessage } from "../hooks/notification.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; -import { ErrorBannerFloat } from "./BankFrame.js"; import { USERNAME_REGEX } from "./RegistrationPage.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; @@ -36,177 +34,202 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { const [password, setPassword] = useState<string | undefined>(); const { i18n } = useTranslationContext(); const testLogin = useCredentialsChecker(); - const [error, saveError] = useState<ErrorMessage | undefined>(); const ref = useRef<HTMLInputElement>(null); useEffect(function focusInput() { ref.current?.focus(); }, []); + const [busy, setBusy] = useState<Record<string, undefined>>() const errors = undefinedIfEmpty({ 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, - }); + }) ?? busy; + + function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) { + notifyError(title, description, debug) + } + + async function doLogin() { + if (!username || !password) return; + setBusy({}) + const testResult = await testLogin(username, password); + if (testResult.valid) { + backend.logIn({ username, password }); + } else { + if (testResult.requestError) { + const { cause } = testResult; + switch (cause.type) { + case ErrorType.CLIENT: { + if (cause.status === HttpStatusCode.Unauthorized) { + saveError({ + title: i18n.str`Wrong credentials for "${username}"`, + }); + } else + if (cause.status === HttpStatusCode.NotFound) { + saveError({ + title: i18n.str`Account not found`, + }); + } else { + saveError({ + title: i18n.str`Could not load due to a client error`, + description: cause.payload.error.description, + debug: JSON.stringify(cause.payload), + }); + } + break; + } + case ErrorType.SERVER: { + saveError({ + title: i18n.str`Server had a problem, try again later or report.`, + description: cause.payload.error.description, + debug: JSON.stringify(cause.payload), + }); + break; + } + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.UNREADABLE: { + saveError({ + title: i18n.str`Unexpected error.`, + description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString, + debug: JSON.stringify(cause), + }); + break; + } + default: { + saveError({ + title: i18n.str`Unexpected error, please report.`, + description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString, + debug: JSON.stringify(cause), + }); + break; + } + } + } else { + saveError({ + title: i18n.str`Unexpected error, please report.`, + debug: JSON.stringify(testResult.error), + }); + } + backend.logOut(); + } + setPassword(undefined); + setBusy(undefined) + } return ( <Fragment> - <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> - {error && ( + <h1 class="nav"></h1> + {/* {error && ( <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} /> - )} - <div class="login-div"> - <form - class="login-form" - noValidate - onSubmit={(e) => { - e.preventDefault(); - }} - autoCapitalize="none" - autoCorrect="off" - > - <div class="pure-form"> - <h2>{i18n.str`Please login!`}</h2> - <p class="unameFieldLabel loginFieldLabel formFieldLabel"> - <label for="username">{i18n.str`Username:`}</label> - </p> - <input - ref={ref} - autoFocus - type="text" - name="username" - id="username" - value={username ?? ""} - enterkeyhint="next" - placeholder="Username" - autocomplete="username" - required - onInput={(e): void => { - setUsername(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.username} - isDirty={username !== undefined} - /> - <p class="passFieldLabel loginFieldLabel formFieldLabel"> - <label for="password">{i18n.str`Password:`}</label> - </p> - <input - type="password" - name="password" - id="password" - autocomplete="current-password" - enterkeyhint="send" - value={password ?? ""} - placeholder="Password" - required - onInput={(e): void => { - setPassword(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.password} - isDirty={password !== undefined} - /> - <br /> - <button - type="submit" - class="pure-button pure-button-primary" - disabled={!!errors} - onClick={async (e) => { - e.preventDefault(); - if (!username || !password) return; - const testResult = await testLogin(username, password); - if (testResult.valid) { - backend.logIn({ username, password }); - } else { - if (testResult.requestError) { - const { cause } = testResult; - switch (cause.type) { - case ErrorType.CLIENT: { - if (cause.status === HttpStatusCode.Unauthorized) { - saveError({ - title: i18n.str`Wrong credentials for "${username}"`, - }); - } - if (cause.status === HttpStatusCode.NotFound) { - saveError({ - title: i18n.str`Account not found`, - }); - } else { - saveError({ - title: i18n.str`Could not load due to a client error`, - description: cause.payload.error.description, - debug: JSON.stringify(cause.payload), - }); - } - break; - } - case ErrorType.SERVER: { - saveError({ - title: i18n.str`Server had a problem, try again later or report.`, - description: cause.payload.error.description, - debug: JSON.stringify(cause.payload), - }); - break; - } - case ErrorType.TIMEOUT: { - saveError({ - title: i18n.str`Request timeout, try again later.`, - }); - break; - } - case ErrorType.UNREADABLE: { - saveError({ - title: i18n.str`Unexpected error.`, - description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}`, - debug: JSON.stringify(cause), - }); - break; - } - default: { - saveError({ - title: i18n.str`Unexpected error, please report.`, - description: `Diagnostic from ${cause.info?.url} is "${cause.message}"`, - debug: JSON.stringify(cause), - }); - break; - } - } - } else { - saveError({ - title: i18n.str`Unexpected error, please report.`, - debug: JSON.stringify(testResult.error), - }); - } - backend.logOut(); - } - setUsername(undefined); - setPassword(undefined); - }} - > - {i18n.str`Login`} - </button> - - {bankUiSettings.allowRegistrations && onRegister ? ( - <button - class="pure-button pure-button-secondary btn-cancel" + )} */} + + <div class="flex min-h-full flex-col justify-center"> + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> + <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2> + </div> + + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> + <form class="space-y-6" noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div> + <label for="username" class="block text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate>Username</i18n.Translate> + </label> + <div class="mt-2"> + <input + ref={ref} + autoFocus + type="text" + name="username" + id="username" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={username ?? ""} + enterkeyhint="next" + placeholder="identification" + autocomplete="username" + required + onInput={(e): void => { + setUsername(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.username} + isDirty={username !== undefined} + /> + </div> + </div> + + <div> + <div class="flex items-center justify-between"> + <label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label> + </div> + <div class="mt-2"> + <input + type="password" + name="password" + id="password" + autocomplete="current-password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + enterkeyhint="send" + value={password ?? ""} + placeholder="Password" + required + onInput={(e): void => { + setPassword(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + <div> + <button type="submit" + class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={!!errors} onClick={(e) => { - e.preventDefault(); - onRegister(); + e.preventDefault() + doLogin() }} > - {i18n.str`Register`} + <i18n.Translate>Log in</i18n.Translate> </button> - ) : ( - <div /> - )} - </div> - </form> + </div> + </form> + + {bankUiSettings.allowRegistrations && onRegister && + <p class="mt-10 text-center text-sm text-gray-500 border-t"> + <button type="submit" + class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" + onClick={(e) => { + e.preventDefault() + onRegister() + }} + > + <i18n.Translate>Register</i18n.Translate> + </button> + </p> + } + </div> </div> + + </Fragment> ); } 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 (<fieldset> <legend class="px-4 text-base font-semibold leading-6 text-gray-900"> @@ -41,34 +41,32 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4"> {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */} <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}> - <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { + <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => { setTab("charge-wallet") }} /> <span class="flex flex-1"> <span class="flex flex-col"> - <span id="project-type-0-label" class="block text-sm font-semibold font-medium text-gray-900"> - <i18n.Translate>a Taler Wallet</i18n.Translate> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate> </span> <span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500"> <i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate> </span> </span> </span> - {/* <!-- Not Checked: "invisible" --> */} <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> </svg> </label> - {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */} <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}> - <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => { + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => { setTab("wire-transfer") }} /> <span class="flex flex-1"> <span class="flex flex-col"> - <span id="project-type-1-label" class="block text-sm font-semibold font-medium text-gray-900"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> <i18n.Translate>another bank account</i18n.Translate> </span> <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> @@ -88,6 +86,9 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { onSuccess={(id) => { updateSettings("currentWithdrawalOperationId", id); }} + onCancel={() => { + setTab(undefined) + }} /> )} {tab === "wire-transfer" && ( @@ -97,32 +98,11 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { onSuccess={() => { notifyInfo(i18n.str`Wire transfer created!`); }} + onCancel={() => { + setTab(undefined) + }} /> )} </fieldset>) - {/* return ( - <article> - <div class="payments"> - <div class="tab"> - <button - class={tab === "charge-wallet" ? "tablinks active" : "tablinks"} - onClick={(): void => { - setTab("charge-wallet"); - }} - > - {i18n.str`Withdraw `} - </button> - <button - class={tab === "wire-transfer" ? "tablinks active" : "tablinks"} - onClick={(): void => { - setTab("wire-transfer"); - }} - > - {i18n.str`Wire transfer`} - </button> - </div> - </div> - </article> - ); */} } 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 (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> <div class="px-4 sm:px-0"> <h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Transfer details</i18n.Translate></h2> @@ -118,7 +168,7 @@ export function PaytoWireTransferForm({ }} /> <span class="flex flex-1"> <span class="flex flex-col"> - <span id="project-type-0-label" class="block text-sm font-medium text-gray-900"> + <span class="block text-sm font-medium text-gray-900"> <i18n.Translate>form</i18n.Translate> </span> </span> @@ -133,7 +183,7 @@ export function PaytoWireTransferForm({ }} /> <span class="flex flex-1"> <span class="flex flex-col"> - <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <span class="block text-sm font-medium text-gray-900"> <i18n.Translate>payto://</i18n.Translate> </span> </span> @@ -143,23 +193,31 @@ export function PaytoWireTransferForm({ </div> </div> - <form class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() + }} + > <div class="px-4 py-6 sm:p-8"> <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> {!isRawPayto ? <Fragment> - <div class="sm:col-span-3"> - <label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Account number`}</label> + <div class="sm:col-span-5"> + <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Account number`}</label> <div class="mt-2"> <input ref={ref} type="text" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - id="iban" name="iban" + id="iban" value={iban ?? ""} placeholder="CC0123456789" + autocomplete="off" required pattern={ibanRegex} onInput={(e): void => { @@ -171,21 +229,18 @@ export function PaytoWireTransferForm({ isDirty={iban !== undefined} /> </div> - <p class="mt-2 text-sm text-gray-500" id="email-description">the receiver of the money</p> - </div> - - <div class="sm:col-span-3"> + <p class="mt-2 text-sm text-gray-500" >the receiver of the money</p> </div> - <div class="sm:col-span-3"> - <label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label> + <div class="sm:col-span-5"> + <label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label> <div class="mt-2"> - <input type="text" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" name="subject" id="subject" + autocomplete="off" placeholder="subject" value={subject ?? ""} required @@ -198,37 +253,40 @@ export function PaytoWireTransferForm({ isDirty={subject !== undefined} /> </div> - <p class="mt-2 text-sm text-gray-500" id="email-description">some text to identify the transfer</p> - - </div> - - <div class="sm:col-span-3"> + <p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p> </div> - <div class="sm:col-span-3"> - <label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label> - <div class="mt-2"> - <input type="text" name="first-name" id="first-name" autocomplete="given-name" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" /> - </div> + <div class="sm:col-span-5"> + <label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label> + <Amount + name="amount" + currency={limit.currency} + value={trimmedAmountStr} + onChange={(d) => { + setAmount(d) + }} + /> + <ShowInputErrorLabel + message={errorsWire?.subject} + isDirty={subject !== undefined} + /> + <p class="mt-2 text-sm text-gray-500" >amount to transfer</p> </div> - <div class="sm:col-span-3"> - </div> </Fragment> : <Fragment> <div class="sm:col-span-6"> - <label for="first-name" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label> + <label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label> <div class="mt-2"> <input name="address" + id="address" type="text" size={50} class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" ref={ref} - id="address" value={rawPaytoInput ?? ""} required placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`} - // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`} onInput={(e): void => { rawPaytoInputSetter(e.currentTarget.value); }} @@ -244,9 +302,23 @@ export function PaytoWireTransferForm({ } </div> </div> - <div class="flex items-center justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - <button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button> - <button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + {onCancel ? + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + onClick={onCancel} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + : <div /> + } + <button type="submit" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={isRawPayto ? !!errorsPayto : !!errorsWire} + onClick={(e) => { + e.preventDefault() + doSend() + }} + > <i18n.Translate>Send</i18n.Translate> </button> </div> @@ -262,8 +334,6 @@ export function PaytoWireTransferForm({ // onSubmit={(e) => { // e.preventDefault(); // }} - // autoCapitalize="none" - // autoCorrect="off" // > // <label for="iban">{i18n.str`Receiver IBAN:`}</label> @@ -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), - // }); - // } - // } + // }} // /> // <input @@ -389,3 +427,46 @@ export function PaytoWireTransferForm({ // </div> // ); } +export function Amount( + { + currency, + name, + value, + error, + onChange, + }: { + error?: string; + currency: string; + name: string; + value: string | undefined; + onChange?: (s: string) => void; + }, + ref: Ref<HTMLInputElement>, +): VNode { + return ( + <div class="mt-2"> + <div class="relative rounded-md shadow-sm"> + <div class="pointer-events-none absolute inset-y-0 flex items-center pl-3"> + <span class="text-gray-500 sm:text-sm">{currency}</span> + </div> + <input + type="number" + class="text-right block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + placeholder="0.00" aria-describedby="price-currency" + ref={ref} + name={name} + id={name} + autocomplete="off" + value={value ?? ""} + disabled={!onChange} + onInput={(e): void => { + if (onChange) { + onChange(e.currentTarget.value); + } + }} + /> + </div> + <ShowInputErrorLabel message={error} isDirty={value !== undefined} /> + </div> + ); +} 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 ( - <section id="main" class="content"> - <h1 class="nav">{i18n.str`Charge your GNU Taler wallet`}</h1> - <article> - <div class="qr-div "> - <a href={talerWithdrawUri} class="pure-button pure-button-primary"> - <i18n.Translate>Continue with GNU Taler</i18n.Translate> - </a> - <p>{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}</p> - <QR text={talerWithdrawUri} /> - <a - class="pure-button btn-cancel" - onClick={async (e) => { - 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`}</a> + <Fragment> + <div class="bg-white shadow-xl sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>If you have a Taler wallet installed in this device</i18n.Translate> + </h3> + <div class="mt-4"> + <a href={talerWithdrawUri} + // class="text-sm font-semibold leading-6 text-gray-900 btn " + class="inline-flex items-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Click here to start</i18n.Translate> + </a> + </div> + <div class="mt-4 max-w-xl text-sm text-gray-500"> + <p><i18n.Translate> + 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 <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">here</a>. + </i18n.Translate></p> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 "> + <div /> + <button type="button" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" + onClick={doAbort} + > + Cancel withdrawal + </button> + </div> + </div> + </div> + + <div class="bg-white shadow-xl sm:rounded-lg mt-8"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Or if you have the wallet in another device</i18n.Translate> + </h3> + <div class="mt-4 max-w-xl text-sm text-gray-500"> + <i18n.Translate>Scan the QR below to start the withdrawal</i18n.Translate> + </div> + <div class="mt-2 max-w-md ml-auto mr-auto"> + <QR text={talerWithdrawUri} /> + </div> + </div> + <div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <button type="button" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" + onClick={doAbort} + > + Cancel withdrawal + </button> </div> - </article> - </section> + </div> + + </Fragment> ); } 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 <http://www.gnu.org/licenses/> */ -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<void> { + return new Promise((resolve) => { + setTimeout(() => { + resolve(undefined); + }, ms) + }) + } + async function doRandomRegistration(tries: number = 3) { + const user = getRandomUsername(); + const pass = getRandomPassword(); + try { + setUsername(undefined); + setPassword(undefined); + setRepeatPassword(undefined); + await register({ username: user, password: pass }); + backend.logIn({ username: user, password: pass }); + onComplete(); + } catch (error) { + if (error instanceof RequestError) { + if (tries > 0) { + await delay(200) + await doRandomRegistration(tries-1) + } else { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`Could not create a random user` + : undefined, + }), + ); + } + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) +} + } + } + return ( <Fragment> - <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> - <article> - <div class="register-div"> - <form - class="register-form" - noValidate + <h1 class="nav"></h1> + + <div class="flex min-h-full flex-col justify-center"> + <div class="sm:mx-auto sm:w-full sm:max-w-sm"> + <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Sign up!`}</h2> + </div> + + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> + <form class="space-y-6" noValidate onSubmit={(e) => { e.preventDefault(); }} autoCapitalize="none" autoCorrect="off" > - <div class="pure-form"> - <h2>{i18n.str`Please register!`}</h2> - <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-un">{i18n.str`Username:`}</label> - </p> - <input - id="register-un" - name="register-un" - type="text" - placeholder="Username" - autocomplete="username" - value={username ?? ""} - onInput={(e): void => { - setUsername(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.username} - isDirty={username !== undefined} - /> - <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-pw">{i18n.str`Password:`}</label> - </p> - <input - type="password" - name="register-pw" - id="register-pw" - placeholder="Password" - autocomplete="new-password" - value={password ?? ""} - required - onInput={(e): void => { - setPassword(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.password} - isDirty={password !== undefined} - /> - <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-repeat">{i18n.str`Repeat Password:`}</label> - </p> - <input - type="password" - style={{ marginBottom: 8 }} - name="register-repeat" - id="register-repeat" - autocomplete="new-password" - placeholder="Same password" - value={repeatPassword ?? ""} - required - onInput={(e): void => { - setRepeatPassword(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.repeatPassword} - isDirty={repeatPassword !== undefined} - /> - <br /> - <button - class="pure-button pure-button-primary btn-register" - type="submit" + <div> + <label for="username" class="block text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate>Username</i18n.Translate> + </label> + <div class="mt-2"> + <input + autoFocus + type="text" + name="username" + id="username" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={username ?? ""} + enterkeyhint="next" + placeholder="identification" + autocomplete="username" + required + onInput={(e): void => { + setUsername(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.username} + isDirty={username !== undefined} + /> + </div> + </div> + + <div> + <div class="flex items-center justify-between"> + <label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label> + </div> + <div class="mt-2"> + <input + type="password" + name="password" + id="password" + autocomplete="current-password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + enterkeyhint="send" + value={password ?? ""} + placeholder="Password" + required + onInput={(e): void => { + setPassword(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + <div> + <div class="flex items-center justify-between"> + <label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900">Repeat assword</label> + </div> + <div class="mt-2"> + <input + type="password" + name="register-repeat" + id="register-repeat" + autocomplete="current-password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + enterkeyhint="send" + value={repeatPassword ?? ""} + placeholder="Same password" + required + onInput={(e): void => { + setRepeatPassword(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.repeatPassword} + isDirty={repeatPassword !== undefined} + /> + </div> + </div> + + <div> + <button type="submit" + class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" disabled={!!errors} - onClick={async (e) => { - e.preventDefault(); - - if (!username || !password) return; - try { - const credentials = { username, password }; - await register(credentials); - setUsername(undefined); - setPassword(undefined); - setRepeatPassword(undefined); - backend.logIn(credentials); - onComplete(); - } catch (error) { - if (error instanceof RequestError) { - notifyError( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`That username is already taken` - : undefined, - }), - ); - } else { - notifyError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); - } - } - }} - > - {i18n.str`Register`} - </button> - {/* FIXME: should use a different color */} - <button - class="pure-button pure-button-secondary btn-cancel" onClick={(e) => { - e.preventDefault(); - setUsername(undefined); - setPassword(undefined); - setRepeatPassword(undefined); - onComplete(); + e.preventDefault() + doRegistrationStep() }} > - {i18n.str`Cancel`} + <i18n.Translate>Confirm</i18n.Translate> </button> </div> + </form> + + <p class="mt-10 text-center text-sm text-gray-500 border-t"> + <button type="submit" + class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" + onClick={(e) => { + e.preventDefault() + doRandomRegistration() + }} + > + <i18n.Translate>Create a random user</i18n.Translate> + </button> + </p> </div> - </article> + </div> + </Fragment> ); } 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 (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2> + <p class="mt-1 text-sm text-gray-500"> + <i18n.Translate>Upon starting you will receive the money in your digital wallet, if you don't have one please <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">install one from here</a></i18n.Translate>. + </p> + <p class="mt-1 text-sm text-gray-500"> + <i18n.Translate>After using your wallet you will be redirected here to confirm or cancel the operation.</i18n.Translate> + </p> + </div> <form - id="reserve-form" - class="pure-form" - name="tform" - onSubmit={(e) => { - 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() + }} > - <p> - <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label> - - <RefAmount - currency={limit.currency} - value={amountStr} - onChange={(v) => { - setAmountStr(v); - }} - error={errors?.amount} - ref={ref} - /> - </p> - <p> - <div> - <input - id="select-exchange" - class="pure-button pure-button-primary" - type="submit" - disabled={!!errors} - value={i18n.str`Withdraw`} - onClick={async (e) => { - 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), - }); - } - } - }} - /> - </div> - </p> - </form> - ); -} + <div class="px-4 py-6 sm:p-8"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label for="withdraw-amount">{i18n.str`Amount`}</label> + <RefAmount + currency={limit.currency} + value={amountStr} + name="withdraw-amount" + onChange={(v) => { + setAmountStr(v); + }} + error={errors?.amount} + ref={ref} + /> + </div> + <div class="sm:col-span-5"> + <span class="isolate inline-flex rounded-md shadow-sm"> + <button type="button" + class="relative inline-flex px-3 py-2 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("50.00") + }} + > + 50.00 + </button> + <button type="button" + class="relative -ml-px -mr-px inline-flex px-3 py-2 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("25.00") + }} + > -export function Amount( - { - currency, - value, - error, - onChange, - }: { - error?: string; - currency: string; - value: string | undefined; - onChange?: (s: string) => void; - }, - ref: Ref<HTMLInputElement>, -): VNode { - return ( - <div style={{ width: "max-content" }}> - <div> - <input - type="text" - readonly - class="currency-indicator" - size={currency.length} - maxLength={currency.length} - tabIndex={-1} - style={{ - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - borderRight: 0, - }} - value={currency} - /> - <input - type="number" - ref={ref} - name="amount" - id="amount" - placeholder="0" - style={{ - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - borderLeft: 0, - width: 150, - color: "black", - }} - value={value ?? ""} - disabled={!onChange} - onInput={(e): void => { - if (onChange) { - onChange(e.currentTarget.value); - } + 25.00 + </button> + <button type="button" + class="relative -ml-px -mr-px inline-flex px-3 py-2 text-sm items-center bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("10.00") + }} + > + 10.00 + </button> + <button type="button" + class="relative inline-flex px-3 py-2 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10" + onClick={(e) => { + e.preventDefault(); + setAmountStr("5.00") + }} + > + 5.00 + </button> + </span> + </div> + + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + onClick={onCancel} + > + <i18n.Translate>Cancel</i18n.Translate></button> + <button type="submit" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + // disabled={isRawPayto ? !!errorsPayto : !!errorsWire} + onClick={(e) => { + e.preventDefault() + doStart() }} - /> + > + <i18n.Translate>Continue</i18n.Translate> + </button> </div> - <ShowInputErrorLabel message={error} isDirty={value !== undefined} /> - </div> + + </form> + </div> ); } + +// export function Amount( +// { +// currency, +// value, +// error, +// onChange, +// }: { +// error?: string; +// currency: string; +// value: string | undefined; +// onChange?: (s: string) => void; +// }, +// ref: Ref<HTMLInputElement>, +// ): VNode { +// return ( +// <div style={{ width: "max-content" }}> +// <div> +// <input +// type="text" +// readonly +// class="currency-indicator" +// size={currency.length} +// maxLength={currency.length} +// tabIndex={-1} +// style={{ +// borderTopRightRadius: 0, +// borderBottomRightRadius: 0, +// borderRight: 0, +// }} +// value={currency} +// /> +// <input +// type="number" +// ref={ref} +// name="amount" +// id="amount" +// placeholder="0" +// style={{ +// borderTopLeftRadius: 0, +// borderBottomLeftRadius: 0, +// borderLeft: 0, +// width: 150, +// color: "black", +// }} +// value={value ?? ""} +// disabled={!onChange} +// onInput={(e): void => { +// if (onChange) { +// onChange(e.currentTarget.value); +// } +// }} +// /> +// </div> +// <ShowInputErrorLabel message={error} isDirty={value !== undefined} /> +// </div> +// ); +// } 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 ( <Fragment> - <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1> - <article> - <div class="challenge-div"> - <form - class="challenge-form" - noValidate - onSubmit={(e) => { - e.preventDefault(); - }} - autoCapitalize="none" - autoCorrect="off" - > - <div class="pure-form" id="captcha" name="capcha-form"> - <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2> - <p> - <label for="answer"> - {i18n.str`What is`} - <em> - {captchaNumbers.a} + {captchaNumbers.b} - </em> - ? - </label> - - <input - name="answer" - id="answer" - value={captchaAnswer ?? ""} - type="text" - autoFocus - required - onInput={(e): void => { - setCaptchaAnswer(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.answer} - isDirty={captchaAnswer !== undefined} - /> - </p> - <p> - <button - type="submit" - class="pure-button pure-button-primary btn-confirm" - disabled={!!errors} - onClick={async (e) => { - e.preventDefault(); - try { - await confirmWithdrawal( - withdrawUri.withdrawalOperationId, - ); - } catch (error) { - if (error instanceof RequestError) { - notifyError( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` - : status === HttpStatusCode.UnprocessableEntity - ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` - : undefined, - }), - ); - } else { - notifyError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); - } - } - }} - > - {i18n.str`Confirm`} - </button> - - <button - class="pure-button pure-button-secondary btn-cancel" - onClick={async (e) => { - 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), - }); + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Confirm the withdrawal operation</i18n.Translate> + </h3> + <div class="mt-2 max-w-xl text-sm text-gray-500"> + <div class="px-4 mt-4 "> + <div class="w-full"> + <div class="px-4 sm:px-0"> + <p><i18n.Translate>Wire transfer details</i18n.Translate></p> + </div> + <div class="mt-6 border-t border-gray-100"> + <dl class="divide-y divide-gray-100"> + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd> + </div> + {name && + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd> + </div> + } + </Fragment> + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd> + </div> + {name && + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd> + </div> + } + </Fragment> + } + default: + return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd> + </div> + } - } - }} - > - {i18n.str`Cancel`} - </button> - </p> + })()} + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.reserve}</dd> + </div> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd> + </div> + </dl> + </div> + </div> + + </div> + <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-3"> + + <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}> + <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>challenge response test</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>using SMS</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>one time password</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + </div> + </div> + <div class="mt-3 text-sm leading-6"> + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer</i18n.Translate></h2> + </div> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() + }} + > + <div class="px-4 py-6 sm:p-8"> + <label for="withdraw-amount">{i18n.str`What is`} + <em> + {captchaNumbers.a} + {captchaNumbers.b} + </em> + ? + </label> + <div class="mt-2"> + <div class="relative rounded-md shadow-sm"> + <input + type="text" + // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + aria-describedby="answer" + autoFocus + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + // value={username ?? ""} + required + + name="answer" + id="answer" + autocomplete="off" + // value={value ?? ""} + // disabled={!onChange} + // onInput={(e): void => { + // if (onChange) { + // onChange(e.currentTarget.value); + // } + // }} + /> + </div> + <ShowInputErrorLabel message={errors?.answer} isDirty={false} /> + </div> + </div> + <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + // onClick={onCancel} + > + <i18n.Translate>Cancel</i18n.Translate></button> + <button type="submit" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + // disabled={isRawPayto ? !!errorsPayto : !!errorsWire} + // onClick={(e) => { + // e.preventDefault() + // doStart() + // }} + > + <i18n.Translate>Transfer</i18n.Translate> + </button> + </div> + + </form> </div> - </form> - <div class="hint"> - <p> - <i18n.Translate> - A this point, a <b>real</b> bank would ask for an additional - authentication proof (PIN/TAN, one time password, ..), instead - of a simple calculation. - </i18n.Translate> - </p> </div> </div> - </article> + </div> + </Fragment> ); } 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({ </section> </section> } + if (!data.selected_reserve_pub) { + return <div> + the exchange is selcted but no reserve pub + </div> + } + + const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) + + if (!account) { + return <div> + the exchange is selcted but no account + </div> + } if (!data.selection_done) { return ( @@ -144,6 +158,11 @@ export function WithdrawalQRCode({ return ( <WithdrawalConfirmationQuestion withdrawUri={withdrawUri} + details={{ + account, + reserve: data.selected_reserve_pub, + amount: Amounts.parseOrThrow("usd:10.00") + }} onAborted={() => { 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 |