diff options
author | Sebastian <sebasjm@gmail.com> | 2023-09-29 16:02:15 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-09-29 16:02:15 -0300 |
commit | 1708d49a2d5da1f325173a030695223e5a24e75c (patch) | |
tree | 048995ac199c171e34fd7d99678c9699fe3321fc | |
parent | c10f3f3aded637111176487f95403294b1164633 (diff) | |
download | wallet-core-1708d49a2d5da1f325173a030695223e5a24e75c.tar.gz wallet-core-1708d49a2d5da1f325173a030695223e5a24e75c.tar.bz2 wallet-core-1708d49a2d5da1f325173a030695223e5a24e75c.zip |
more ui
24 files changed, 448 insertions, 470 deletions
diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs index 9c09e5716..f29a05e49 100755 --- a/packages/demobank-ui/dev.mjs +++ b/packages/demobank-ui/dev.mjs @@ -18,7 +18,7 @@ import { serve } from "@gnu-taler/web-util/node"; import { initializeDev } from "@gnu-taler/web-util/build"; -const devEntryPoints = ["src/stories.tsx", "src/index.tsx"]; +const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/demobank-ui-settings.js"]; const build = initializeDev({ type: "development", diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx new file mode 100644 index 000000000..3313e5796 --- /dev/null +++ b/packages/demobank-ui/src/components/Attention.tsx @@ -0,0 +1,59 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { assertUnreachable } from "./Routing.js"; + +interface Props { + type?: "info" | "success" | "warning" | "danger", + onClose?: () => void, + title: TranslatedString, + children?: ComponentChildren , +} +export function Attention({ type = "info", title, children, onClose }: Props): VNode { + return <div class={`group attention-${type} mt-2`}> + <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> + <div class="flex"> + <div > + <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400"> + {(() => { + switch (type) { + case "info": + return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" /> + case "warning": + return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "danger": + return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "success": + return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /> + default: + assertUnreachable(type) + } + })()} + </svg> + </div> + <div class="ml-3 w-full"> + <h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800"> + {title} + </h3> + <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700"> + {children} + </div> + </div> + {onClose && + <div> + <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" + onClick={(e) => { + e.preventDefault(); + onClose(); + }} + > + <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> + </div> + } + </div> + </div> + + </div> +} diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx index f83b61234..ee62671ce 100644 --- a/packages/demobank-ui/src/components/ErrorLoading.tsx +++ b/packages/demobank-ui/src/components/ErrorLoading.tsx @@ -17,25 +17,13 @@ import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode { const { i18n } = useTranslationContext() - return ( - <div><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">{error.message}</p> - </div> - </div> - <div class="ml-3 flex-1 md:flex md:justify-between"> - <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p> - </div> - </div> - </div> + return (<Attention type="danger" title={error.message as TranslatedString}> + <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p> + </Attention> ); } diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index f8b2e3113..f92c874f3 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { State } from "./index.js"; import { format, isToday } from "date-fns"; import { Amounts } from "@gnu-taler/taler-util"; +import { useEffect, useRef } from "preact/hooks"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -55,9 +56,9 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <thead> <tr> <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> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th> + <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th> </tr> </thead> <tbody> @@ -69,22 +70,38 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </th> </tr> {txs.map(item => { + const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss") + const amount = <Fragment> + {item.negative ? "-" : ""} + {item.amount ? ( + `${Amounts.stringifyValue(item.amount)} ${item.amount.currency + }` + ) : ( + <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> + )} + </Fragment> return (<tr key={idx}> <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, "HH:mm:ss")}</div> + <div class="font-medium text-gray-900">{time}</div> + <dl class="font-normal sm:hidden"> + <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-700"> + {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( + `${Amounts.stringifyValue(item.amount)}` + ) : ( + <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> + )}</dd> + <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-500 sm:hidden"> + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} + </dd> + </dl> </td> <td data-negative={item.negative ? "true" : "false"} - class="px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> - {item.negative ? "-" : ""} - {item.amount ? ( - `${Amounts.stringifyValue(item.amount)} ${item.amount.currency - }` - ) : ( - <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> + class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> + {amount} + </td> + <td class="hidden sm:table-cell 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 break-all min-w-md">{item.subject}</td> </tr>) })} @@ -94,8 +111,8 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </tbody> </table> - - <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination"> + + <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination"> <div class="flex flex-1 justify-between sm:justify-end"> <button class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js new file mode 100644 index 000000000..8a0961831 --- /dev/null +++ b/packages/demobank-ui/src/demobank-ui-settings.js @@ -0,0 +1,21 @@ +// Values for development environment + +/** + * Global settings for the demobank UI. + */ +localStorage.setItem("bank-base-url", "http://bank.taler.test/"); + +globalThis.talerDemobankSettings = { + backendBaseURL: "http://bank.taler.test/", + allowRegistrations: true, + showDemoNav: true, + simplePasswordForRandomAccounts: true, + allowRandomAccountCreation: true, + bankName: "Taler DEVELOPMENT Bank", + // Names and links for other demo sites to show in the navbar + demoSites: [ + ["Exchange", "https://Exchnage.taler.test/"], + ["Bank", "https://bank-ui.taler.test/"], + ["Merchant", "https://merchant.taler.test/"], + ], +}; diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 20fd64bfa..154c43ae6 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -70,7 +70,7 @@ export function useAccessAPI(): AccessAPI { contentType: "json", }, ); - await mutateAll(/.*accounts\/.*\/transactions.*/); + await mutateAll(/.*accounts\/.*/); return res; }; const deleteAccount = async (): Promise<HttpResponseOk<void>> => { @@ -382,7 +382,6 @@ export function useTransactions( loadMore: () => { if (!afterData || isReachingEnd) return; // if (afterData.data.transactions.length < MAX_RESULT_SIZE) { - // console.log("load more", page) const l = afterData.data.transactions[afterData.data.transactions.length-1] setStart(String(l.row_id)); // } diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 82caafdf2..5dba60951 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -435,7 +435,7 @@ export function useBusinessAccounts( HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, RequestError<SandboxBackend.SandboxError> >( - [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], + [`accounts`, args?.page, PAGE_SIZE, args?.account], sandboxAccountsFetcher, { refreshInterval: 0, diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 5f004c6d4..ad853f9d7 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -33,6 +33,7 @@ interface Settings { showInstallWallet: boolean; maxWithdrawalAmount: number; fastWithdrawal: boolean; + showDebugInfo: boolean; } export const codecForSettings = (): Codec<Settings> => @@ -42,6 +43,7 @@ export const codecForSettings = (): Codec<Settings> => .property("showDemoDescription", (codecForBoolean())) .property("showInstallWallet", (codecForBoolean())) .property("fastWithdrawal", (codecForBoolean())) + .property("showDebugInfo", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); @@ -52,6 +54,7 @@ const defaultSettings: Settings = { showInstallWallet: true, maxWithdrawalAmount: 25, fastWithdrawal: false, + showDebugInfo: false, }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index 83846be90..483cb579a 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -16,10 +16,10 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; +import { Attention } from "../../components/Attention.js"; import { Transactions } from "../../components/Transactions/index.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useSettings } from "../../hooks/settings.js"; -import { bankUiSettings } from "../../settings.js"; import { PaymentOptions } from "../PaymentOptions.js"; import { State } from "./index.js"; @@ -31,53 +31,27 @@ export function InvalidIbanView({ error }: State.InvalidIban) { const IS_PUBLIC_ACCOUNT_ENABLED = false - function ShowDemoInfo(): VNode { const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings(); if (!settings.showDemoDescription) return <Fragment /> - return <div class="rounded-md bg-blue-50 p-4"> - <div class="flex"> - <div class="flex-shrink-0"> - <svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> - </svg> - </div> - <div class="ml-3"> - <h3 class="text-sm font-bold text-blue-800"> - <i18n.Translate>This is a demo bank!</i18n.Translate> - </h3> - <div class="mt-2 text-sm text-blue-700"> - {IS_PUBLIC_ACCOUNT_ENABLED ? ( - <i18n.Translate> - This part of the demo shows how a bank that supports Taler - directly would work. In addition to using your own bank - account, you can also see the transaction history of some{" "} - <a href="/public-accounts">Public Accounts</a>. - </i18n.Translate> - ) : ( - <i18n.Translate> - This part of the demo shows how a bank that supports Taler - directly would work. - </i18n.Translate> - )} - <p class="mt-3 text-sm flex justify-end"> - <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(); - updateSettings("showDemoDescription", false); - }} - > - <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> - </div> - </div> + return <Attention title={i18n.str`This is a demo bank`} onClose={() => { + updateSettings("showDemoDescription", false); + }}> + {IS_PUBLIC_ACCOUNT_ENABLED ? ( + <i18n.Translate> + This part of the demo shows how a bank that supports Taler + directly would work. In addition to using your own bank + account, you can also see the transaction history of some{" "} + <a href="/public-accounts">Public Accounts</a>. + </i18n.Translate> + ) : ( + <i18n.Translate> + This part of the demo shows how a bank that supports Taler + directly would work. + </i18n.Translate> + )} + </Attention> } export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 15ef8a036..29334cae4 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,7 +15,7 @@ */ import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { NotificationMessage, notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks"; import { LangSelector } from "../components/LangSelector.js"; @@ -26,6 +26,7 @@ import { useSettings } from "../hooks/settings.js"; import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import logo from "../assets/logo-2021.svg"; import { useAccountDetails } from "../hooks/access.js"; +import { Attention } from "../components/Attention.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -108,7 +109,7 @@ export function BankFrame({ setOpen(!open) }}> <span class="absolute -inset-0.5"></span> - <span class="sr-only">Open main menu</span> + <span class="sr-only">Open settings</span> <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> @@ -231,6 +232,22 @@ export function BankFrame({ <div class="flex items-center justify-between"> <span class="flex flex-grow flex-col"> <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <i18n.Translate>Show debug info</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showDebugInfo} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + + onClick={() => { + updateSettings("showDebugInfo", !settings.showDebugInfo); + }}> + <span aria-hidden="true" data-enabled={settings.showDebugInfo} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + </div> + </li> + <li class="mt-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span class="text-sm text-black font-medium leading-6 " id="availability-label"> <i18n.Translate>Show install wallet first</i18n.Translate> </span> </span> @@ -286,10 +303,10 @@ export function BankFrame({ } </div > + <StatusBanner /> <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"> - <StatusBanner /> {children} </div> </div> @@ -301,79 +318,46 @@ export function BankFrame({ ); } +function MaybeShowDebugInfo({ info }: { info: any }): VNode { + const [settings] = useSettings() + if (settings.showDebugInfo) { + return <pre class="whitespace-break-spaces "> + {info} + </pre> + } + return <Fragment /> +} + function StatusBanner(): VNode { const notifs = useNotifications() - return <div - class="fixed top-10 z-20 ml-4 mr-4" - > { + if (notifs.length === 0) return <Fragment /> + return <div class="fixed z-20 w-full p-4"> { notifs.map(n => { 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> - </div> - <div> - <p class="text-sm"> - <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> + return <Attention type="danger" title={n.message.title} onClose={() => { + n.remove() + }}> {n.message.description && <div class="mt-2 text-sm text-red-700"> {n.message.description} </div> } + <MaybeShowDebugInfo info={n.message.debug} /> + {/* <a href="#" class="text-gray-500"> + show debug info + </a> {n.message.debug && <div class="mt-2 text-sm text-red-700 font-mono break-all"> {n.message.debug} </div> - } - </div> + } */} + </Attention> 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(); - }} - > - <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> + return <Attention type="success" title={n.message.title} onClose={() => { + n.remove(); + }} /> } })} </div> diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index d945d80d1..95144f086 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -137,8 +137,8 @@ export function handleNotOkResult( const errorData = result.payload; notify({ type: "error", - title: i18n.str`Could not load due to a client error`, - description: errorData?.error?.description as TranslatedString, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`, debug: JSON.stringify(result), }); break; @@ -174,7 +174,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - route("/") + // route("/") return <div>error</div>; } return <div />; diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 14d261622..3ea94b899 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -23,6 +23,7 @@ import { useBackendContext } from "../context/backend.js"; import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; +import { doAutoFocus } from "./PaytoWireTransferForm.js"; /** @@ -98,8 +99,8 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { }); } else { saveError({ - title: i18n.str`Could not load due to a client error`, - // description: cause.payload.error.description, + title: i18n.str`Could not load due to a request error`, + description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`, debug: JSON.stringify(cause.payload), }); } @@ -159,8 +160,7 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { </label> <div class="mt-2"> <input - ref={ref} - autoFocus + ref={doAutoFocus} type="text" name="username" id="username" diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 56e79f9ab..4be680377 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -118,7 +118,9 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive try { setBusy({}) await confirmWithdrawal(wid); - notifyInfo(i18n.str`Wire transfer completed!`) + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`) + } } catch (error) { if (error instanceof RequestError) { notify( diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index 93b3694d7..2cb7385db 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -267,13 +267,12 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) { </div> <div class="mt-3 text-center sm:mt-5"> <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> - <i18n.Translate>Withdrawal OK</i18n.Translate> + <i18n.Translate>Withdrawal confirmed</i18n.Translate> </h3> <div class="mt-2"> <p class="text-sm text-gray-500"> <i18n.Translate> - The wire transfer to the Taler exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. + The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet. </i18n.Translate> </p> </div> diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 49419d0dc..fef272831 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ const { i18n } = useTranslationContext(); const [settings] = useSettings(); - const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); + const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer"); return ( <div class="mt-2"> @@ -82,7 +82,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ <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"> - <i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate> + <i18n.Translate>Make a wire transfer to an account which you know the bank account number</i18n.Translate> </span> </span> </span> @@ -108,6 +108,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ limit={limit} onSuccess={() => { notifyInfo(i18n.str`Wire transfer created!`); + setTab(undefined) }} onCancel={() => { setTab(undefined) diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 5f5a6ce3b..785dc4264 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -55,10 +55,11 @@ export function PaytoWireTransferForm({ onCancel: (() => void) | undefined; limit: AmountJson; }): VNode { - const [isRawPayto, setIsRawPayto] = useState(false); - const [iban, setIban] = useState<string | undefined>(undefined); - const [subject, setSubject] = useState<string | undefined>(undefined); - const [amount, setAmount] = useState<string | undefined>(undefined); + const [isRawPayto, setIsRawPayto] = useState(true); + // FIXME: remove this + const [iban, setIban] = useState<string | undefined>("DE4745461198061"); + const [subject, setSubject] = useState<string | undefined>("ASD"); + const [amount, setAmount] = useState<string | undefined>("1.00001"); const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>( undefined, @@ -76,17 +77,17 @@ export function PaytoWireTransferForm({ const errorsWire = undefinedIfEmpty({ iban: !iban - ? i18n.str`Missing IBAN` + ? i18n.str`required` : !IBAN_REGEX.test(iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(iban, i18n), - subject: !subject ? i18n.str`Missing subject` : undefined, + subject: !subject ? i18n.str`required` : undefined, amount: !trimmedAmountStr - ? i18n.str`Missing amount` + ? i18n.str`required` : !parsedAmount - ? i18n.str`Amount is not valid` + ? i18n.str`not valid` : Amounts.isZero(parsedAmount) - ? i18n.str`Should be greater than 0` + ? i18n.str`should be greater than 0` : Amounts.cmp(limit, parsedAmount) === -1 ? i18n.str`balance is not enough` : undefined, @@ -101,14 +102,14 @@ export function PaytoWireTransferForm({ ? i18n.str`required` : !parsed ? i18n.str`does not follow the pattern` - : !parsed.params.amount - ? i18n.str`use the "amount" parameter to specify the amount to be transferred` - : Amounts.parse(parsed.params.amount) === undefined - ? i18n.str`the amount is not valid` - : !parsed.params.message - ? i18n.str`use the "message" parameter to specify a reference text for the transfer` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`only "IBAN" target are supported` + : !parsed.isKnown || parsed.targetType !== "iban" + ? i18n.str`only "IBAN" target are supported` + : !parsed.params.amount + ? i18n.str`use the "amount" parameter to specify the amount to be transferred` + : Amounts.parse(parsed.params.amount) === undefined + ? i18n.str`the amount is not valid` + : !parsed.params.message + ? i18n.str`use the "message" parameter to specify a reference text for the transfer` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(parsed.iban, i18n), @@ -159,6 +160,9 @@ export function PaytoWireTransferForm({ } 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"> + {/** + * FIXME: Scan a qr code + */} <div class="px-4 sm:px-0"> <h2 class="text-base font-semibold leading-7 text-gray-900"> {title} @@ -167,6 +171,17 @@ export function PaytoWireTransferForm({ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4"> <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "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={() => { + if (parsed && parsed.isKnown && parsed.targetType === "iban") { + setIban(parsed.iban) + const amount = Amounts.parse(parsed.params["amount"]) + if (amount) { + setAmount(Amounts.stringifyValue(amount)) + } + const subject = parsed.params["subject"] + if (subject) { + setSubject(subject) + } + } setIsRawPayto(false) }} /> <span class="flex flex-1"> @@ -180,12 +195,22 @@ export function PaytoWireTransferForm({ <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "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={() => { + if (iban) { + const payto = buildPayto("iban", iban, undefined) + if (parsedAmount) { + payto.params["amount"] = Amounts.stringify(parsedAmount) + } + if (subject) { + payto.params["message"] = subject + } + rawPaytoInputSetter(stringifyPaytoUri(payto)) + } setIsRawPayto(true) }} /> <span class="flex flex-1"> <span class="flex flex-col"> <span class="block text-sm font-medium text-gray-900"> - <i18n.Translate>using the payto:// format</i18n.Translate> + <i18n.Translate>Import payto:// URI</i18n.Translate> </span> </span> </span> @@ -195,7 +220,7 @@ export function PaytoWireTransferForm({ </div> <form - class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 w-fit mx-auto" autoCapitalize="none" autoCorrect="off" onSubmit={e => { @@ -203,105 +228,106 @@ export function PaytoWireTransferForm({ }} > <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-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" - name="iban" - id="iban" - value={iban ?? ""} - placeholder="CC0123456789" - autocomplete="off" - required - pattern={ibanRegex} - onInput={(e): void => { - setIban(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errorsWire?.iban} - isDirty={iban !== undefined} - /> - </div> - <p class="mt-2 text-sm text-gray-500" >the receiver of the money</p> - </div> + {!isRawPayto ? + <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <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 - onInput={(e): void => { - setSubject(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errorsWire?.subject} - isDirty={subject !== undefined} - /> - </div> - <p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p> + <div class="sm:col-span-5"> + <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</label> + <div class="mt-2"> + <input + ref={focus ? doAutoFocus : undefined} + 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="iban" + id="iban" + value={iban ?? ""} + placeholder="CC0123456789" + autocomplete="off" + required + pattern={ibanRegex} + onInput={(e): void => { + setIban(e.currentTarget.value.toUpperCase()); + }} + /> + <ShowInputErrorLabel + message={errorsWire?.iban} + isDirty={iban !== undefined} + /> </div> + <p class="mt-2 text-sm text-gray-500" > + <i18n.Translate>IBAN of the recipient's account</i18n.Translate> + </p> + </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) + <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 + onInput={(e): void => { + setSubject(e.currentTarget.value); }} /> <ShowInputErrorLabel message={errorsWire?.subject} isDirty={subject !== undefined} /> - <p class="mt-2 text-sm text-gray-500" >amount to transfer</p> </div> + <p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p> + </div> - </Fragment> : - <Fragment> - <div class="sm:col-span-6"> - <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} - value={rawPaytoInput ?? ""} - required - placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`} - onInput={(e): void => { - rawPaytoInputSetter(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errorsPayto?.rawPaytoInput} - isDirty={rawPaytoInput !== undefined} - /> - </div> - </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" + left + currency={limit.currency} + value={trimmedAmountStr} + onChange={(d) => { + setAmount(d) + }} + /> + <ShowInputErrorLabel + message={errorsWire?.amount} + isDirty={subject !== undefined} + /> + <p class="mt-2 text-sm text-gray-500" >amount to transfer</p> + </div> - </Fragment> - } - </div> + </div> : + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full"> + <div class="sm:col-span-6"> + <label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label> + <div class="mt-2"> + <textarea + ref={focus ? doAutoFocus : undefined} + name="address" + id="address" + type="textarea" + rows={3} + class="block overflow-hidden w-64 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={rawPaytoInput ?? ""} + required + placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`} + onInput={(e): void => { + rawPaytoInputSetter(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errorsPayto?.rawPaytoInput} + isDirty={rawPaytoInput !== undefined} + /> + </div> + </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"> {onCancel ? @@ -328,17 +354,37 @@ export function PaytoWireTransferForm({ ) } + +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocus(element: HTMLElement | null) { + if (element) { + window.requestIdleCallback(() => { + element.focus() + element.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center" + }) + }) + } +} + export function Amount( { currency, name, value, error, + left, onChange, }: { error?: string; currency: string; name: string; + left?: boolean | undefined, value: string | undefined; onChange?: (s: string) => void; }, @@ -346,13 +392,16 @@ export function Amount( ): 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"> + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + <div + class="pointer-events-none inset-y-0 flex items-center px-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" + data-left={left} + class="text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6" placeholder="0.00" aria-describedby="price-currency" ref={ref} name={name} @@ -371,3 +420,4 @@ export function Amount( </div> ); } + diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 0a5a386ae..6a50d4ef3 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -86,7 +86,6 @@ export function QrCodeSection({ </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> diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx index d19c411f3..46f4fe0ef 100644 --- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { doAutoFocus } from "./PaytoWireTransferForm.js"; export function UpdateAccountPassword({ account, @@ -27,11 +28,6 @@ export function UpdateAccountPassword({ const [password, setPassword] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>(); - const ref = useRef<HTMLInputElement>(null); - useEffect(() => { - if (focus) ref.current?.focus(); - }, [focus]); - if (!result.ok) { if (result.loading || result.type === ErrorType.TIMEOUT) { return onLoadNotOk(result); @@ -96,7 +92,7 @@ export function UpdateAccountPassword({ </label> <div class="mt-2"> <input - ref={ref} + ref={focus ? doAutoFocus : undefined} type="password" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" name="password" diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 3c5ee34fd..7357223b7 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -29,14 +29,15 @@ import { notifyError, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { forwardRef } from "preact/compat"; import { useEffect, useRef, useState } from "preact/hooks"; import { useAccessAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { Amount } from "./PaytoWireTransferForm.js"; +import { Amount, doAutoFocus } from "./PaytoWireTransferForm.js"; import { useSettings } from "../hooks/settings.js"; import { OperationState } from "./OperationState/index.js"; +import { Attention } from "../components/Attention.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); @@ -53,47 +54,13 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { const { createWithdrawal } = useAccessAPI(); const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`); - const ref = useRef<HTMLInputElement>(null); - useEffect(() => { - if (focus) ref.current?.focus(); - }, [focus]); if (!!settings.currentWithdrawalOperationId) { - return <div> - - <div class="rounded-md bg-yellow-50 ring-yellow-2 p-4"> - <div class="flex"> - <div class="flex-shrink-0"> - <svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> - </svg> - </div> - <div class="ml-3"> - <h3 class="text-sm font-bold text-yellow-800"> - <i18n.Translate>There is an operation already</i18n.Translate> - </h3> - <div class="mt-2 text-sm text-yellow-700"> - <p> - <i18n.Translate> - To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a> - </i18n.Translate> - </p> - </div> - - </div> - </div> - </div > - <div class="flex 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 bg-white p-2 rounded-sm" - onClick={() => { - updateSettings("currentWithdrawalOperationId", undefined) - onCancel() - }} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - </div> - </div> + return <Attention type="warning" title={i18n.str`There is an operation already`}> + <i18n.Translate> + To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a> + </i18n.Translate> + </Attention> } const trimmedAmountStr = amountStr?.trim(); @@ -157,8 +124,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { 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"> + <div class="px-4 py-6 "> + <div class="grid max-w-xs 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 @@ -169,51 +136,53 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { setAmountStr(v); }} error={errors?.amount} - ref={ref} + ref={focus ? doAutoFocus : undefined} /> </div> - <div class="sm:col-span-5"> - <span class="isolate inline-flex rounded-md shadow-sm"> - <button type="button" - class="relative inline-flex px-6 py-4 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-6 py-4 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") - }} - > + </div> + <div class="mt-4"> + <div class="sm:inline"> - 25.00 - </button> - <button type="button" - class="relative -ml-px -mr-px inline-flex px-6 py-4 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-6 py-4 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> + <button type="button" + class=" inline-flex px-6 py-4 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=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none 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") + }} + > + 25.00 + </button> + </div> + <div class="mt-4 sm:inline"> + <button type="button" + class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none 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=" inline-flex px-6 py-4 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> + </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"> @@ -255,46 +224,20 @@ export function WalletWithdrawForm({ <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>After using your wallet you will confirm or cancel the operation.</i18n.Translate> + <i18n.Translate>After using your wallet you will need to confirm or cancel the operation on this site.</i18n.Translate> </p> </div> <div class="col-span-2"> - {settings.showInstallWallet && <div class="rounded-md bg-blue-50 ring-blue-2 ring-2 p-4"> - <div class="flex"> - <div class="flex-shrink-0"> - <svg class="h-5 w-5 text-blue-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /> - </svg> - </div> - <div class="ml-3"> - <h3 class="text-sm font-bold text-blue-800"> - <i18n.Translate>You need a GNU Taler Wallet</i18n.Translate> - </h3> - <div class="mt-2 text-sm text-blue-700"> - <p> - <i18n.Translate> - If you dont have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a> - </i18n.Translate> - </p> - <p class="mt-3 text-sm flex justify-end"> - <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(); - updateSettings("showInstallWallet", false); - }} - > - I know - <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> - </div> - </div>} + {settings.showInstallWallet && + <Attention title={i18n.str`You need a GNU Taler Wallet`} onClose={() => { + updateSettings("showInstallWallet", false); + }}> + <i18n.Translate> + If you don't have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a> + </i18n.Translate> + </Attention> + } {!settings.fastWithdrawal ? <OldWithdrawalForm diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index d160a88b3..208d4b859 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -37,6 +37,7 @@ import { useMemo, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAccessAnonAPI } from "../hooks/access.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { useSettings } from "../hooks/settings.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -59,6 +60,7 @@ export function WithdrawalConfirmationQuestion({ withdrawUri, }: Props): VNode { const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() const captchaNumbers = useMemo(() => { return { @@ -87,7 +89,9 @@ export function WithdrawalConfirmationQuestion({ await confirmWithdrawal( withdrawUri.withdrawalOperationId, ); - notifyInfo(i18n.str`Wire transfer completed!`) + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`) + } } catch (error) { if (error instanceof RequestError) { notify( @@ -203,7 +207,7 @@ export function WithdrawalConfirmationQuestion({ <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 text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer</i18n.Translate></h2> + <h2 class="text-base font-semibold 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" @@ -312,13 +316,9 @@ export function WithdrawalConfirmationQuestion({ } })()} <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 break-words">{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)} + {Amounts.currencyOf(details.amount)} {Amounts.stringifyValue(details.amount)} </dd> </div> </dl> diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 8f4e175f6..c8efc033b 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -94,20 +94,12 @@ export function WithdrawalQRCode({ </div> <div class="mt-3 text-center sm:mt-5"> <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> - <i18n.Translate>Withdrawal OK</i18n.Translate> + <i18n.Translate>Withdrawal confirmed</i18n.Translate> </h3> <div class="mt-2"> <p class="text-sm text-gray-500"> <i18n.Translate> - The wire transfer to the Taler exchange bank's account is completed, now the - exchange will send the requested amount into your GNU Taler wallet. - </i18n.Translate> - </p> - </div> - <div class="mt-2"> - <p > - <i18n.Translate> - You can close this page now or continue to the account page. + The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet. </i18n.Translate> </p> </div> diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index 02df824a2..ed8bf610d 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -4,6 +4,7 @@ import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty import { useEffect, useRef, useState } from "preact/hooks"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const EMAIL_REGEX = @@ -37,10 +38,6 @@ export function AccountForm({ RecursivePartial<typeof initial> | undefined >(undefined); const { i18n } = useTranslationContext(); - const ref = useRef<HTMLInputElement>(null); - useEffect(() => { - if (focus) ref.current?.focus(); - }, [focus]); function updateForm(newForm: typeof initial): void { @@ -97,7 +94,6 @@ export function AccountForm({ <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 class="block text-sm font-medium leading-6 text-gray-900" @@ -108,7 +104,7 @@ export function AccountForm({ </label> <div class="mt-2"> <input - ref={ref} + ref={focus ? doAutoFocus : undefined} type="text" class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" name="username" diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx index ffa559097..d50ff14b4 100644 --- a/packages/demobank-ui/src/pages/admin/Home.tsx +++ b/packages/demobank-ui/src/pages/admin/Home.tsx @@ -10,6 +10,7 @@ import { AdminAccount } from "./Account.js"; import { AccountList } from "./AccountList.js"; import { CreateNewAccount } from "./CreateNewAccount.js"; import { RemoveAccount } from "./RemoveAccount.js"; +import { Transactions } from "../../components/Transactions/index.js"; /** * Query account information and show QR code if there is pending withdrawal @@ -141,6 +142,7 @@ export function AdminHome({ onRegister }: Props): VNode { <AdminAccount onRegister={onRegister} /> + <Transactions account="admin"/> </Fragment> ); }
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 1e5370afc..b323b0d01 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -6,6 +6,8 @@ import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; import { useEffect, useRef, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { Attention } from "../../components/Attention.js"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; export function RemoveAccount({ account, @@ -36,47 +38,15 @@ export function RemoveAccount({ } return onLoadNotOk(result); } - const ref = useRef<HTMLInputElement>(null); - useEffect(() => { - if (focus) ref.current?.focus(); - }, [focus]); - const balance = Amounts.parse(result.data.balance.amount); if (!balance) { return <div>there was an error reading the balance</div>; } const isBalanceEmpty = Amounts.isZero(balance); if (!isBalanceEmpty) { - return <div> - <div class="rounded-md bg-yellow-50 p-4"> - <div class="flex"> - <div class="flex-shrink-0"> - <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> - </svg> - </div> - <div class="ml-3"> - <h3 class="text-sm font-medium text-yellow-800"> - <i18n.Translate>Can't delete the account</i18n.Translate> - </h3> - <div class="mt-2 text-sm text-yellow-700"> - <p> - <i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate> - </p> - </div> - </div> - - </div> - </div> - <div class="mt-2 flex justify-end"> - <button type="button" class="rounded-md ring-1 ring-gray-400 bg-white px-3 py-2 text-sm font-semibold shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 " - onClick={() => { - onCancel() - }}> - <i18n.Translate>Go back</i18n.Translate> - </button> - </div> - </div> + return <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}> + <i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate> + </Attention> } async function doRemove() { @@ -117,26 +87,9 @@ export function RemoveAccount({ return ( <div> - <div class="rounded-md bg-yellow-50 p-4"> - <div class="flex"> - <div class="flex-shrink-0"> - <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> - </svg> - </div> - <div class="ml-3"> - <h3 class="text-sm font-bold text-yellow-800"> - <i18n.Translate>You are going to remove the account</i18n.Translate> - </h3> - <div class="mt-2 text-sm text-yellow-700"> - <p> - <i18n.Translate>This step can't be undone.</i18n.Translate> - </p> - </div> - </div> - - </div> - </div> + <Attention type="warning" title={i18n.str`You are going to remove the account`}> + <i18n.Translate>This step can't be undone.</i18n.Translate> + </Attention> <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"> @@ -164,7 +117,7 @@ export function RemoveAccount({ </label> <div class="mt-2"> <input - ref={ref} + ref={focus ? doAutoFocus : undefined} 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 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" name="password" |