diff options
-rw-r--r-- | packages/demobank-ui/src/components/Routing.tsx | 12 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/settings.ts | 8 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/views.tsx | 50 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BankFrame.tsx | 133 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/HomePage.tsx | 20 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaymentOptions.tsx | 137 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/QrCodeSection.tsx | 106 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WalletWithdrawForm.tsx | 183 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 159 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/admin/RemoveAccount.tsx | 2 |
10 files changed, 553 insertions, 257 deletions
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index b8e39948b..e1fd93737 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -76,9 +76,9 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} - onLoadNotOk={() => { - route("/account"); - }} + // onLoadNotOk={() => { + // route("/account"); + // }} /> )} /> @@ -108,9 +108,9 @@ export function Routing(): VNode { } else { return <HomePage account={username} - onPendingOperationFound={(wopid) => { - route(`/operation/${wopid}`); - }} + // onPendingOperationFound={(wopid) => { + // route(`/operation/${wopid}`); + // }} goToBusinessAccount={() => { route("/business"); }} diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 43e803726..c2fd93a0c 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -29,20 +29,26 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; interface Settings { currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; + showDemoDescription: boolean; maxWithdrawalAmount: number; + fastWithdrawal: boolean; } export const codecForSettings = (): Codec<Settings> => buildCodecForObject<Settings>() .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) + .property("showDemoDescription", (codecForBoolean())) + .property("fastWithdrawal", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, - maxWithdrawalAmount: 25 + showDemoDescription: true, + maxWithdrawalAmount: 25, + fastWithdrawal: 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 abd14848f..0187989af 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -23,6 +23,7 @@ import { State } from "./index.js"; import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; +import { useSettings } from "../../hooks/settings.js"; export function InvalidIbanView({ error }: State.InvalidIban) { return ( @@ -78,9 +79,58 @@ function ImportantMessage(): VNode { } +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"> + <p> + <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> + </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("showDemoDescription", false); + }} + > + 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> + </div> +</div> +} + export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { + const { i18n } = useTranslationContext(); + return <Fragment> <MaybeBusinessButton account={account} onClick={goToBusinessAccount} /> + + <ShowDemoInfo /> + <PaymentOptions limit={limit} /> <Transactions account={account} /> </Fragment>; diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 4b23686d6..d1c94135b 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -27,7 +27,6 @@ import { CopyButton, CopyIcon } from "../components/CopyButton.js"; import logo from "../assets/logo-2021.svg"; import { useAccountDetails } from "../hooks/access.js"; -const IS_PUBLIC_ACCOUNT_ENABLED = false; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -142,24 +141,40 @@ export function BankFrame({ <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> - <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> + <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 <li> + <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> + </li> + })} + </ul> + </li> + + <li> + <ul role="list" class="space-y-1"> + <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"> @@ -169,29 +184,43 @@ export function BankFrame({ <button type="button" data-enabled={settings.showWithdrawalSuccess} 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={() => { - console.log(settings.showWithdrawalSuccess) updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess); }}> <span aria-hidden="true" data-enabled={settings.showWithdrawalSuccess} 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> - </ul> - </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 demo description</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showDemoDescription} 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" - <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> - })} + onClick={() => { + updateSettings("showDemoDescription", !settings.showDemoDescription); + }}> + <span aria-hidden="true" data-enabled={settings.showDemoDescription} 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>Use fast withdrawal</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.fastWithdrawal} 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("fastWithdrawal", !settings.fastWithdrawal); + }}> + <span aria-hidden="true" data-enabled={settings.fastWithdrawal} 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> </ul> </li> </ul> @@ -240,11 +269,6 @@ export function BankFrame({ // > // <a href="#main" class="skip">{i18n.str`Skip to main content`}</a> // <div style="max-width: 50em; margin-left: 2em; margin-right: 2em;"> - // <h1> - // <span class="it"> - // <a href="/">{bankUiSettings.bankName}</a> - // </span> - // </h1> // {maybeDemoContent( // <p> // {IS_PUBLIC_ACCOUNT_ENABLED ? ( @@ -298,33 +322,6 @@ export function BankFrame({ ); } -// 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> -// ); -// } function StatusBanner(): VNode { const notifs = useNotifications() @@ -469,5 +466,5 @@ function AccountBalance({ account }: { account: string }): VNode { {Amounts.currencyOf(result.data.balance.amount)} {result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} {Amounts.stringifyValue(result.data.balance.amount)} - </div> + </div> } diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 40cc147a6..2acfc9b57 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -52,21 +52,21 @@ const logger = new Logger("AccountPage"); export function HomePage({ onRegister, account, - onPendingOperationFound, + // onPendingOperationFound, goToBusinessAccount, }: { account: string, - onPendingOperationFound: (id: string) => void; + // onPendingOperationFound: (id: string) => void; onRegister: () => void; goToBusinessAccount: () => void; }): VNode { const [settings] = useSettings(); const { i18n } = useTranslationContext(); - if (settings.currentWithdrawalOperationId) { - onPendingOperationFound(settings.currentWithdrawalOperationId); - return <Loading />; - } + // if (settings.currentWithdrawalOperationId) { + // onPendingOperationFound(settings.currentWithdrawalOperationId); + // return <Loading />; + // } return ( <AccountPage @@ -79,11 +79,9 @@ export function HomePage({ export function WithdrawalOperationPage({ operationId, - onLoadNotOk, onContinue, }: { operationId: string; - onLoadNotOk: () => void; onContinue: () => void; }): VNode { //FIXME: libeufin sandbox should return show to create the integration api endpoint @@ -95,6 +93,7 @@ export function WithdrawalOperationPage({ }); const parsedUri = parseWithdrawUri(uri); const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings(); if (!parsedUri) { notifyError( @@ -107,8 +106,9 @@ export function WithdrawalOperationPage({ return ( <WithdrawalQRCode withdrawUri={parsedUri} - onContinue={onContinue} - onLoadNotOk={onLoadNotOk} + onClose={() => { + updateSettings("currentWithdrawalOperationId", undefined) + }} /> ); } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 1728074a3..573f8c769 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -34,80 +34,83 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { // 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"> - <i18n.Translate>Send money to</i18n.Translate> - </legend> + <div class="mt-2"> - <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" onClick={() => { - setTab("charge-wallet") - }} /> - <span class="flex flex-1"> - <div class="text-lg mr-2">💵</div> - <span class="flex flex-col"> - <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> + <fieldset> + <legend class="px-4 text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Send money to</i18n.Translate> + </legend> + + <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" onClick={() => { + setTab("charge-wallet") + }} /> + <span class="flex flex-1"> + <div class="text-lg mr-2">💵</div> + <span class="flex flex-col"> + <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> - </span> - <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> + <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> - <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" onClick={() => { - setTab("wire-transfer") - }} /> - <span class="flex flex-1"> - <div class="text-lg mr-2">↔</div> - <span class="flex flex-col"> - <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"> - <i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate> + <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" onClick={() => { + setTab("wire-transfer") + }} /> + <span class="flex flex-1"> + <div class="text-lg mr-2">↔</div> + <span class="flex flex-col"> + <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"> + <i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate> + </span> </span> </span> - </span> - <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "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> - </div> - {tab === "charge-wallet" && ( - <WalletWithdrawForm - focus - limit={limit} - onSuccess={(id) => { - updateSettings("currentWithdrawalOperationId", id); - }} - onCancel={() => { - setTab(undefined) - }} - /> - )} - {tab === "wire-transfer" && ( - <PaytoWireTransferForm - focus - title={i18n.str`Transfer details`} - limit={limit} - onSuccess={() => { - notifyInfo(i18n.str`Wire transfer created!`); - }} - onCancel={() => { - setTab(undefined) - }} - /> - )} + <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "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> + </div> + {tab === "charge-wallet" && ( + <WalletWithdrawForm + focus + limit={limit} + onSuccess={(id) => { + updateSettings("currentWithdrawalOperationId", id); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} + {tab === "wire-transfer" && ( + <PaytoWireTransferForm + focus + title={i18n.str`Transfer details`} + limit={limit} + onSuccess={() => { + notifyInfo(i18n.str`Wire transfer created!`); + }} + onCancel={() => { + setTab(undefined) + }} + /> + )} - </fieldset> + </fieldset> + </div> ) } diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 7c1b3bdc5..416c714e2 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -135,3 +135,109 @@ export function QrCodeSection({ </Fragment> ); } + + +export function QrCodeSectionSimpler({ + withdrawUri, + onAborted, +}: { + withdrawUri: WithdrawUriResult; + onAborted: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + useEffect(() => { + //Taler Wallet WebExtension is listening to headers response and tab updates. + //In the SPA there is no header response with the Taler URI so + //this hack manually triggers the tab update after the QR is in the DOM. + // WebExtension will be using + // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated + document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`; + }, []); + 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 ( + <Fragment> + <div class="bg-white shadow-xl sm:rounded-lg"> + <div class="p2 "> + <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> + </div> + + </Fragment> + ); +} + diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 8c41f7576..08f706919 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -20,6 +20,7 @@ import { HttpStatusCode, Logger, TranslatedString, + WithdrawUriResult, parseWithdrawUri, } from "@gnu-taler/taler-util"; import { @@ -34,6 +35,9 @@ 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 { useSettings } from "../hooks/settings.js"; +import { WithdrawalOperationState } from "./WithdrawalQRCode.js"; +import { Loading } from "../components/Loading.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); @@ -51,8 +55,9 @@ export function WalletWithdrawForm({ }): VNode { const { i18n } = useTranslationContext(); const { createWithdrawal } = useAccessAPI(); + const [settings, updateSettings] = useSettings() - const [amountStr, setAmountStr] = useState<string | undefined>("5.00"); + const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`); const ref = useRef<HTMLInputElement>(null); useEffect(() => { if (focus) ref.current?.focus(); @@ -78,7 +83,6 @@ export function WalletWithdrawForm({ async function doStart() { if (!parsedAmount) return; try { - console.log("ASDASD") const result = await createWithdrawal({ amount: Amounts.stringify(parsedAmount), }); @@ -109,7 +113,6 @@ export function WalletWithdrawForm({ ) } } - } 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"> @@ -122,91 +125,103 @@ export function WalletWithdrawForm({ <i18n.Translate>After using your wallet you will be redirected here to confirm or cancel the operation.</i18n.Translate> </p> </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"> - <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-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") - }} - > - - 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") + + {!settings.fastWithdrawal ? + <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"> + <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); }} - > - 5.00 - </button> - </span> - </div> + 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-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") + }} + > + + 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> + </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> - </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> - </form> + </form> + : settings.currentWithdrawalOperationId === undefined ? + <Loading /> : + <WithdrawalOperationState + currentOperation={settings.currentWithdrawalOperationId} + currency={limit.currency} + onClose={() => { + onCancel() + }} + /> + } </div> ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 2a3a1ec2c..9976babdb 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -18,24 +18,29 @@ import { Amounts, HttpStatusCode, Logger, + TranslatedString, WithdrawUriResult, parsePaytoUri, + parseWithdrawUri, + stringifyWithdrawUri, } from "@gnu-taler/taler-util"; -import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, RequestError, notify, notifyError, 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 { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; -import { QrCodeSection } from "./QrCodeSection.js"; +import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; +import { useEffect, useState } from "preact/hooks"; +import { buildRequestErrorMessage } from "../utils.js"; +import { getInitialBackendBaseURL } from "../hooks/backend.js"; const logger = new Logger("WithdrawalQRCode"); interface Props { withdrawUri: WithdrawUriResult; - onContinue: () => void; - onLoadNotOk: () => void; + onClose: () => void; } /** * Offer the QR code (and a clickable taler://-link) to @@ -44,14 +49,9 @@ interface Props { */ export function WithdrawalQRCode({ withdrawUri, - onContinue, - onLoadNotOk, + onClose, }: Props): VNode { const [settings, updateSettings] = useSettings(); - function clearCurrentWithdrawal(): void { - updateSettings("currentWithdrawalOperationId", undefined); - onContinue(); - } const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); if (!result.ok) { @@ -62,7 +62,7 @@ export function WithdrawalQRCode({ result.type === ErrorType.CLIENT && result.status === HttpStatusCode.NotFound ) { - clearCurrentWithdrawal() + onClose() return <div>operation not found</div>; } // onLoadNotOk(); @@ -89,8 +89,7 @@ export function WithdrawalQRCode({ style={{ float: "right" }} onClick={async (e) => { e.preventDefault(); - clearCurrentWithdrawal() - onContinue() + onClose() }}> {i18n.str`Continue`} </a> @@ -149,8 +148,7 @@ export function WithdrawalQRCode({ class="inline-flex w-full justify-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" onClick={async (e) => { e.preventDefault(); - clearCurrentWithdrawal() - onContinue() + onClose() }}> <i18n.Translate>Continue</i18n.Translate> </button> @@ -165,8 +163,7 @@ export function WithdrawalQRCode({ withdrawUri={withdrawUri} onAborted={() => { notifyInfo(i18n.str`Operation canceled`); - clearCurrentWithdrawal() - onContinue() + onClose() }} /> ); @@ -196,9 +193,131 @@ export function WithdrawalQRCode({ }} onAborted={() => { notifyInfo(i18n.str`Operation canceled`); - clearCurrentWithdrawal() - onContinue() + onClose() }} /> ); +} + + +export function WithdrawalOperationState({ + currency, + currentOperation, + onClose, +}: {currency:string, currentOperation: string, onClose: () => void}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() + const { createWithdrawal } = useAccessAPI(); + + const amount = settings.maxWithdrawalAmount + async function doSilentStart() { + //FIXME: if amount is not enough use balance + const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) + + 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 { + updateSettings("currentWithdrawalOperationId", 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 + ) + } + } + } + + useEffect(() => { + doSilentStart() + }, [settings.fastWithdrawal, amount]) + + const result = useWithdrawalDetails(currentOperation); + if (!result.ok) { + if (result.loading) { + return <Loading />; + } + if ( + result.type === ErrorType.CLIENT && + result.status === HttpStatusCode.NotFound + ) { + onClose() + return <div>operation not found</div>; + } + // onLoadNotOk(); + return handleNotOkResult(i18n)(result); + } + const { data } = result; + + const baseUrl = getInitialBackendBaseURL() + const uri = stringifyWithdrawUri({ + bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`, + withdrawalOperationId: currentOperation, + }); + const parsedUri = parseWithdrawUri(uri); + + if (data.aborted) { + return <div> + the operation was aborted, you can create another one + </div> + } + + if (data.confirmation_done) { + return <div> + the wire transfer is made, you coin should arrive shortly + </div> + } + if (!parsedUri) { + return <div> + the operation is not valid, create another one + </div> + } + if (!data.selection_done) { + return ( + <QrCodeSectionSimpler + withdrawUri={parsedUri} + onAborted={() => { + notifyInfo(i18n.str`Operation canceled`); + onClose() + }} + /> + ); + } + + 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 selected but no account + </div> + } + + return <div> + the operation is wating for the question to be answered + </div>; }
\ 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 050f1fb8a..1e5370afc 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -61,7 +61,7 @@ export function RemoveAccount({ </h3> <div class="mt-2 text-sm text-yellow-700"> <p> - <i18n.Translate>The account can be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate> + <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> |