diff options
Diffstat (limited to 'packages')
21 files changed, 4444 insertions, 1097 deletions
| diff --git a/packages/demobank-ui/src/assets/logo-2021.svg b/packages/demobank-ui/src/assets/logo-2021.svg new file mode 100644 index 000000000..8c5ff3e5b --- /dev/null +++ b/packages/demobank-ui/src/assets/logo-2021.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90"> +  <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3"> +    <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" /> +    <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" /> +    <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" /> +  </g> +  <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" /> +</svg>
\ No newline at end of file diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/demobank-ui/src/components/QR.tsx index c1c159ef8..945a08867 100644 --- a/packages/demobank-ui/src/components/QR.tsx +++ b/packages/demobank-ui/src/components/QR.tsx @@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode {    return (      <div        style={{ -        width: "100%",          display: "flex",          flexDirection: "column",          alignItems: "left", @@ -41,9 +40,7 @@ export function QR({ text }: { text: string }): VNode {      >        <div          style={{ -          width: "50%", -          minWidth: 200, -          maxWidth: 300, +          width: "100%",            marginRight: "auto",            marginLeft: "auto",          }} diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index f4a78e516..6303037a1 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -39,21 +39,21 @@ export function ReadyView({ transactions }: State.Ready): VNode {            <h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>          </div>        </div> -      <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg"> +      <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg min-w-fit bg-white">          <table class="min-w-full divide-y divide-gray-300">            <thead>              <tr> -              <th scope="col" class="pl-4 pr-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">{i18n.str`Date`}</th> -              <th scope="col" class="px-3      py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Amount`}</th> -              <th scope="col" class="px-3      py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Counterpart`}</th> -              <th scope="col" class="px-3      py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Subject`}</th> +              <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th> +              <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Amount`}</th> +              <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Counterpart`}</th> +              <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">{i18n.str`Subject`}</th>              </tr>            </thead>            <tbody>              {transactions.map((item, idx) => {                return (                  <tr key={idx}> -                  <td class="relative py-4 pl-4 pr-3 text-sm sm:pl-6"> +                  <td class="relative py-2 pl-2 pr-2 text-sm ">                      <div class="font-medium text-gray-900">{item.when.t_ms === "never"                        ? ""                        : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}</div> @@ -66,7 +66,7 @@ export function ReadyView({ transactions }: State.Ready): VNode {                        <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span>                      )}</td>                    <td class="px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td> -                  <td class="px-3 py-3.5 text-sm text-gray-500">{item.subject}</td> +                  <td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td>                  </tr>                );              })} diff --git a/packages/demobank-ui/src/forms/simplest.ts b/packages/demobank-ui/src/forms/simplest.ts new file mode 100644 index 000000000..54b6b1c65 --- /dev/null +++ b/packages/demobank-ui/src/forms/simplest.ts @@ -0,0 +1,66 @@ +import { +  AbsoluteTime, +  AmountJson, +  TranslatedString +} from "@gnu-taler/taler-util"; +import { DoubleColumnForm, FormState } from "@gnu-taler/web-util/browser"; + +export namespace Data { +  export interface WithResolution { +    when: AbsoluteTime; +    threshold: AmountJson; +    state: string; +  } +  export interface Form extends WithResolution { +    comment: string; +  } +} + +const design: DoubleColumnForm = [ +  { +    title: "Simple form" as TranslatedString, +    fields: [ +      { +        type: "textArea", +        props: { +          name: "comment", +          label: "Comments" as TranslatedString, +        }, +      }, +    ], +  }, +  { +    title: "Resolution" as TranslatedString, +    description: `Current state is and threshold at ` as TranslatedString, +    fields: [ +      { +        type: "date", +        props: { +          name: "when", +          label: "Decision Time" as TranslatedString, +        }, +      }, +      { +        type: "amount", +        props: { +          name: "threshold", +          label: "New threshold" as TranslatedString, +        }, +      }, +    ], +  } +  , +]; + +function formBehavior(v: Partial<Data.Form>): FormState<Data.Form> { +  return { +    when: { +      disabled: true, +    }, +    threshold: { +      // disabled: v.state === AmlExchangeBackend.AmlState.frozen, +    }, +  }; +} + + diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts deleted file mode 100644 index 9bf621b41..000000000 --- a/packages/demobank-ui/src/hooks/notification.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { memoryMap } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; - -export type NotificationMessage = ErrorNotification | InfoNotification; - -//FIXME: this should not be exported since every notification -// goes throw notify function -export interface ErrorMessage { -  description?: string; -  title: TranslatedString; -  debug?: string; -} - -interface ErrorNotification { -  type: "error"; -  error: ErrorMessage; -} -interface InfoNotification { -  type: "info"; -  info: TranslatedString; -} - -const storage = memoryMap<NotificationMessage>(); -const NOTIFICATION_KEY = "notification"; - -export function onNotificationUpdate( -  handler: (newValue: NotificationMessage | undefined) => void, -) { -  return storage.onUpdate(NOTIFICATION_KEY, () => { -    const newValue = storage.get(NOTIFICATION_KEY); -    handler(newValue); -  }); -} - -export function notifyError(error: ErrorMessage) { -  storage.set(NOTIFICATION_KEY, { type: "error", error }); -} -export function notifyInfo(info: TranslatedString) { -  storage.set(NOTIFICATION_KEY, { type: "info", info }); -} - -export function useNotifications(): [ -  NotificationMessage | undefined, -  StateUpdater<NotificationMessage | undefined>, -] { -  const [value, setter] = useState<NotificationMessage | undefined>(); -  useEffect(() => { -    return storage.onUpdate(NOTIFICATION_KEY, () => { -      setter(storage.get(NOTIFICATION_KEY)); -    }); -  }); -  return [value, setter]; -} diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 28fb7cb0c..ed6945f84 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -15,7 +15,7 @@   */  import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser"; -import { AbsoluteTime, AmountJson, PaytoUriIBAN } from "@gnu-taler/taler-util"; +import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util";  import { Loading } from "../../components/Loading.js";  import { useComponentState } from "./state.js";  import { ReadyView, InvalidIbanView} from "./views.js"; @@ -51,7 +51,7 @@ export namespace State {      status: "ready";      error: undefined;      account: string,  -    payto: PaytoUriIBAN,  +    payto: PaytoUriIBAN | PaytoUriTalerBank,       balance: AmountJson,       balanceIsDebit: boolean,       limit: AmountJson, diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index bc59c9374..2249b743e 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -15,10 +15,9 @@   */  import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; -import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";  import { useBackendContext } from "../../context/backend.js";  import { useAccountDetails } from "../../hooks/access.js"; -import { notifyError } from "../../hooks/notification.js";  import { Props, State } from "./index.js";  export function useComponentState({ account, onLoadNotOk }: Props): State { @@ -43,9 +42,7 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {      //logout if there is any error, not if loading      backend.logOut();      if (result.status === HttpStatusCode.NotFound) { -      notifyError({ -        title: i18n.str`Username or account label "${account}" not found`, -      }); +      notifyError(i18n.str`Username or account label "${account}" not found`, undefined);        return {          status: "error-user-not-found",          error: result, @@ -62,7 +59,8 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {    const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);    const payto = parsePaytoUri(data.paytoUri); -  if (!payto || !payto.isKnown || payto.targetType !== "iban") { +  if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) { +    console.log(payto)      return {        status: "invalid-iban",        error: result diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index 73a4f9ca3..18462bdc3 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -14,11 +14,14 @@   GNU Taler; see the file COPYING.  If not, see <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 diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 4ce0f140e..c13b9a3cb 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -16,11 +16,12 @@  import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";  import { +  ErrorNotification,    ErrorType,    HttpError,    useTranslationContext,  } from "@gnu-taler/web-util/browser"; -import { ErrorMessage } from "./hooks/notification.js"; +  /**   * Validate (the number part of) an amount.  If needed, @@ -120,11 +121,12 @@ export function buildRequestErrorMessage(      onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;      onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;    } = {}, -): ErrorMessage { -  let result: ErrorMessage; +): ErrorNotification { +  let result: ErrorNotification;    switch (cause.type) {      case ErrorType.TIMEOUT: {        result = { +        type: "error",          title: i18n.str`Request timeout`,        };        break; @@ -133,8 +135,9 @@ export function buildRequestErrorMessage(        const title =          specialCases.onClientError && specialCases.onClientError(cause.status);        result = { +        type: "error",          title: title ? title : i18n.str`The server didn't accept the request`, -        description: cause?.payload?.error?.description, +        description: cause?.payload?.error?.description as TranslatedString,          debug: JSON.stringify(cause),        };        break; @@ -143,24 +146,27 @@ export function buildRequestErrorMessage(        const title =          specialCases.onServerError && specialCases.onServerError(cause.status);        result = { +        type: "error",          title: title            ? title            : i18n.str`The server had problems processing the request`, -        description: cause?.payload?.error?.description, +        description: cause?.payload?.error?.description as TranslatedString,          debug: JSON.stringify(cause),        };        break;      }      case ErrorType.UNREADABLE: {        result = { +        type: "error",          title: i18n.str`Unexpected error`, -        description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`, +        description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString,          debug: JSON.stringify(cause),        };        break;      }      case ErrorType.UNEXPECTED: {        result = { +        type: "error",          title: i18n.str`Unexpected error`,          debug: JSON.stringify(cause),        }; | 
