diff options
Diffstat (limited to 'packages/demobank-ui')
-rw-r--r-- | packages/demobank-ui/src/components/Routing.tsx | 27 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/settings.ts | 3 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/index.ts | 2 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/state.ts | 3 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage/views.tsx | 4 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BankFrame.tsx | 15 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/HomePage.tsx | 17 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/OperationState/index.ts | 10 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/OperationState/state.ts | 109 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/OperationState/views.tsx | 363 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaymentOptions.tsx | 17 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/QrCodeSection.tsx | 104 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WalletWithdrawForm.tsx | 273 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 163 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/business/Home.tsx | 1 |
15 files changed, 693 insertions, 418 deletions
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index e1fd93737..90d2d4c48 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -46,6 +46,20 @@ export function Routing(): VNode { )} /> <Route + path="/operation/:wopid" + component={({ wopid }: { wopid: string }) => ( + <WithdrawalOperationPage + operationId={wopid} + onContinue={() => { + route("/account"); + }} + // onLoadNotOk={() => { + // route("/account"); + // }} + /> + )} + /> + <Route path="/register" component={() => ( <RegistrationPage @@ -65,10 +79,6 @@ export function Routing(): VNode { <BankFrame account={backend.state.username}> <Router history={history}> <Route - path="/test" - component={Test} - /> - <Route path="/operation/:wopid" component={({ wopid }: { wopid: string }) => ( <WithdrawalOperationPage @@ -76,9 +86,6 @@ export function Routing(): VNode { onContinue={() => { route("/account"); }} - // onLoadNotOk={() => { - // route("/account"); - // }} /> )} /> @@ -108,9 +115,9 @@ export function Routing(): VNode { } else { return <HomePage account={username} - // onPendingOperationFound={(wopid) => { - // route(`/operation/${wopid}`); - // }} + goToConfirmOperation={(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 c2fd93a0c..5f004c6d4 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -30,6 +30,7 @@ interface Settings { currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; showDemoDescription: boolean; + showInstallWallet: boolean; maxWithdrawalAmount: number; fastWithdrawal: boolean; } @@ -39,6 +40,7 @@ export const codecForSettings = (): Codec<Settings> => .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) .property("showDemoDescription", (codecForBoolean())) + .property("showInstallWallet", (codecForBoolean())) .property("fastWithdrawal", (codecForBoolean())) .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); @@ -47,6 +49,7 @@ const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, showDemoDescription: true, + showInstallWallet: true, maxWithdrawalAmount: 25, fastWithdrawal: false, }; diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts index 128a6d30f..81eeb4a03 100644 --- a/packages/demobank-ui/src/pages/AccountPage/index.ts +++ b/packages/demobank-ui/src/pages/AccountPage/index.ts @@ -29,6 +29,7 @@ export interface Props { error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, ) => VNode; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; } export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound; @@ -54,6 +55,7 @@ export namespace State { account: string, limit: AmountJson, goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; } export interface InvalidIban { diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts index a57e19901..1a1475c0d 100644 --- a/packages/demobank-ui/src/pages/AccountPage/state.ts +++ b/packages/demobank-ui/src/pages/AccountPage/state.ts @@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAccountDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; -export function useComponentState({ account, goToBusinessAccount }: Props): State { +export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State { const result = useAccountDetails(account); const backend = useBackendContext(); const { i18n } = useTranslationContext(); @@ -75,6 +75,7 @@ export function useComponentState({ account, goToBusinessAccount }: Props): Stat return { status: "ready", goToBusinessAccount, + goToConfirmOperation, error: undefined, account, limit, diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx index 0187989af..23a815bd8 100644 --- a/packages/demobank-ui/src/pages/AccountPage/views.tsx +++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx @@ -123,7 +123,7 @@ function ShowDemoInfo():VNode { </div> } -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); return <Fragment> @@ -131,7 +131,7 @@ export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): <ShowDemoInfo /> - <PaymentOptions limit={limit} /> + <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} /> <Transactions account={account} /> </Fragment>; } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index d1c94135b..5bfaa63ec 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -210,6 +210,21 @@ 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 install wallet first</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={settings.showInstallWallet} 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("showInstallWallet", !settings.showInstallWallet); + }}> + <span aria-hidden="true" data-enabled={settings.showInstallWallet} 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> diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 2acfc9b57..8d5e1f3b9 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -36,6 +36,7 @@ import { useSettings } from "../hooks/settings.js"; import { AccountPage } from "./AccountPage/index.js"; import { LoginForm } from "./LoginForm.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; +import { route } from "preact-router"; const logger = new Logger("AccountPage"); @@ -52,25 +53,20 @@ const logger = new Logger("AccountPage"); export function HomePage({ onRegister, account, - // onPendingOperationFound, + goToConfirmOperation, goToBusinessAccount, }: { account: string, - // onPendingOperationFound: (id: string) => void; onRegister: () => void; goToBusinessAccount: () => void; + goToConfirmOperation: (id:string) => void; }): VNode { - const [settings] = useSettings(); const { i18n } = useTranslationContext(); - // if (settings.currentWithdrawalOperationId) { - // onPendingOperationFound(settings.currentWithdrawalOperationId); - // return <Loading />; - // } - return ( <AccountPage account={account} + goToConfirmOperation={goToConfirmOperation} goToBusinessAccount={goToBusinessAccount} onLoadNotOk={handleNotOkResult(i18n, onRegister)} /> @@ -102,12 +98,13 @@ export function WithdrawalOperationPage({ ); return <Loading />; } - + return ( <WithdrawalQRCode withdrawUri={parsedUri} onClose={() => { updateSettings("currentWithdrawalOperationId", undefined) + onContinue() }} /> ); @@ -178,7 +175,7 @@ export function handleNotOkResult( assertUnreachable(result); } } - + route("/") return <div>error</div>; } return <div />; diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts index 254fcba5f..32302f272 100644 --- a/packages/demobank-ui/src/pages/OperationState/index.ts +++ b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -26,6 +26,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js"; export interface Props { currency: string; onClose: () => void; + goToConfirmOperation: (id: string) => void; } export type State = State.Loading | @@ -57,26 +58,33 @@ export namespace State { error: undefined; uri: WithdrawUriResult, onClose: () => void; + onAbort: () => void; } export interface InvalidPayto { status: "invalid-payto", error: undefined; payto: string | null; + onClose: () => void; } export interface InvalidWithdrawal { status: "invalid-withdrawal", error: undefined; + onClose: () => void; uri: string, } export interface InvalidReserve { status: "invalid-reserve", error: undefined; + onClose: () => void; reserve: string | null; } export interface NeedConfirmation { status: "need-confirmation", + onAbort: () => void; + onConfirm: () => void; error: undefined; + busy: boolean, } export interface Aborted { status: "aborted", @@ -111,7 +119,7 @@ const viewMapping: utils.StateViewMap<State> = { ready: ReadyView, }; -export const AccountPage = utils.compose( +export const OperationState = utils.compose( (p: Props) => useComponentState(p), viewMapping, ); diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 6fb7bb28f..ae03ed529 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -15,21 +15,24 @@ */ import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; +import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; import { useBackendContext } from "../../context/backend.js"; -import { useAccessAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; +import { useAccessAPI, useAccessAnonAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js"; import { Props, State } from "./index.js"; import { useSettings } from "../../hooks/settings.js"; -import { buildRequestErrorMessage } from "../../utils.js"; -import { useEffect } from "preact/hooks"; +import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; import { getInitialBackendBaseURL } from "../../hooks/backend.js"; -export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> { +export function useComponentState({ currency, onClose,goToConfirmOperation }: Props): utils.RecursiveState<State> { const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings() const { createWithdrawal } = useAccessAPI(); + const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI(); + const [busy, setBusy] = useState<Record<string, undefined>>() const amount = settings.maxWithdrawalAmount + async function doSilentStart() { //FIXME: if amount is not enough use balance const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) @@ -67,12 +70,14 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const withdrawalOperationId = settings.currentWithdrawalOperationId useEffect(() => { - doSilentStart() + if (withdrawalOperationId === undefined) { + doSilentStart() + } }, [settings.fastWithdrawal, amount]) const baseUrl = getInitialBackendBaseURL() - const withdrawalOperationId = settings.currentWithdrawalOperationId if (!withdrawalOperationId) { return { @@ -81,6 +86,63 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } } + const wid = withdrawalOperationId + + async function doAbort() { + try { + setBusy({}) + await abortWithdrawal(wid); + onClose(); + } 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 + ) + } + } + setBusy(undefined) + } + + async function doConfirm() { + try { + setBusy({}) + await confirmWithdrawal(wid); + notifyInfo(i18n.str`Wire transfer completed!`) + } catch (error) { + if (error instanceof RequestError) { + notify( + buildRequestErrorMessage(i18n, error.cause, { + onClientError: (status) => + status === HttpStatusCode.Conflict + ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` + : status === HttpStatusCode.UnprocessableEntity + ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` + : undefined, + }), + ); + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } + setBusy(undefined) + } const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api` const uri = stringifyWithdrawUri({ bankIntegrationApiBaseUrl, @@ -92,11 +154,13 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "invalid-withdrawal", error: undefined, uri, + onClose, } } return (): utils.RecursiveState<State> => { const result = useWithdrawalDetails(withdrawalOperationId); + if (!result.ok) { if (result.loading) { return { @@ -119,10 +183,17 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } if (data.confirmation_done) { + if (!settings.showWithdrawalSuccess) { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + } return { status: "confirmed", error: undefined, - onClose, + onClose: async () => { + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, } } @@ -131,7 +202,12 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "ready", error: undefined, uri: parsedUri, - onClose + onClose: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + onAbort: doAbort, } } @@ -139,7 +215,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-reserve", error: undefined, - reserve: data.selected_reserve_pub + reserve: data.selected_reserve_pub, + onClose, } } @@ -149,13 +226,23 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive return { status: "invalid-payto", error: undefined, - payto: data.selected_exchange_account + payto: data.selected_exchange_account, + onClose, } } + + // goToConfirmOperation(withdrawalOperationId) return { status: "need-confirmation", error: undefined, + onAbort: async () => { + await doAbort() + updateSettings("currentWithdrawalOperationId", undefined) + onClose() + }, + busy: !!busy, + onConfirm: doConfirm } } diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index db25eaf61..17f1d8457 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, stringifyPaytoUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { Transactions } from "../../components/Transactions/index.js"; @@ -24,42 +24,375 @@ import { CopyButton } from "../../components/CopyButton.js"; import { bankUiSettings } from "../../settings.js"; import { useBusinessAccountDetails } from "../../hooks/circuit.js"; import { useSettings } from "../../hooks/settings.js"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import { undefinedIfEmpty } from "../../utils.js"; +import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { QR } from "../../components/QR.js"; -export function InvalidPaytoView({ error }: State.InvalidPayto) { +export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <div>Payto from server is not valid "{payto}"</div> ); } -export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) { +export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <div>Withdrawal uri from server is not valid "{uri}"</div> ); } -export function InvalidReserveView({ error }: State.InvalidReserve) { +export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <div>Reserve from server is not valid "{reserve}"</div> ); } -export function NeedConfirmationView({ error }: State.NeedConfirmation) { +export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) { + const { i18n } = useTranslationContext() + + const captchaNumbers = useMemo(() => { + return { + a: Math.floor(Math.random() * 10), + b: Math.floor(Math.random() * 10), + }; + }, []); + const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>(); + const answer = parseInt(captchaAnswer ?? "", 10); + const errors = undefinedIfEmpty({ + answer: !captchaAnswer + ? i18n.str`Answer the question before continue` + : Number.isNaN(answer) + ? i18n.str`The answer should be a number` + : answer !== captchaNumbers.a + captchaNumbers.b + ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.` + : undefined, + }) ?? (busy ? {} as Record<string, undefined> : undefined); + return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold text-gray-900"> + <i18n.Translate>Confirm the withdrawal operation</i18n.Translate> + </h3> + <div class="mt-2 max-w-xl text-sm text-gray-500"> + <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-3"> + + <label class={"relative sm:col-span-2 flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}> + <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>challenge response test</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>using SMS</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>one time password</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + </div> + </div> + <div class="mt-3 text-sm leading-6"> + + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() + }} + > + <div class="px-4 py-6 sm:p-8"> + <label for="withdraw-amount">{i18n.str`What is`} + <em> + {captchaNumbers.a} + {captchaNumbers.b} + </em> + ? + </label> + <div class="mt-2"> + <div class="relative rounded-md shadow-sm"> + <input + type="text" + // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + aria-describedby="answer" + autoFocus + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={captchaAnswer ?? ""} + required + + name="answer" + id="answer" + autocomplete="off" + onChange={(e): void => { + setCaptchaAnswer(e.currentTarget.value) + }} + /> + </div> + <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} /> + </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={onAbort} + > + <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={!!errors} + onClick={(e) => { + e.preventDefault() + onConfirm() + }} + > + <i18n.Translate>Transfer</i18n.Translate> + </button> + </div> + + </form> + </div> + <div class="px-4 mt-4 "> + {/* <div class="w-full"> + <div class="px-4 sm:px-0 text-sm"> + <p><i18n.Translate>Wire transfer details</i18n.Translate></p> + </div> + <div class="mt-6 border-t border-gray-100"> + <dl class="divide-y divide-gray-100"> + {((): VNode => { + switch (details.account.targetType) { + case "iban": { + const p = details.account as PaytoUriIBAN + const name = p.params["receiver-name"] + return <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd> + </div> + {name && + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd> + </div> + } + </Fragment> + } + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd> + </div> + {name && + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd> + </div> + } + </Fragment> + } + default: + return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd> + </div> + + } + })()} + <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">To be added</dd> + // {/* Amounts.stringifyValue(details.amount) + </div> + </dl> + </div> + </div> */} + + </div> + </div> + </div> + ); } -export function AbortedView({ error }: State.Aborted) { +export function AbortedView({ error, onClose }: State.Aborted) { return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <div>aborted</div> ); } -export function ConfirmedView({ error }: State.Confirmed) { +export function ConfirmedView({ error, onClose }: State.Confirmed) { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() return ( - <div>Payto from server is not valid "{error.data.paytoUri}"</div> + <Fragment> + + <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all "> + + <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100"> + <svg class="h-6 w-6 text-green-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="M4.5 12.75l6 6 9-13.5" /> + </svg> + </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> + </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> + </div> + <div class="mt-4"> + <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>Do not show this again</i18n.Translate> + </span> + </span> + <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={() => { + 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> + </div> + <div class="mt-5 sm:mt-6"> + <button type="button" + 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(); + onClose() + }}> + <i18n.Translate>Close</i18n.Translate> + </button> + </div> + </Fragment> + ); } -export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> { +export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); - return <div /> + 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} ${uri.withdrawalOperationId}`; + }, []); + const talerWithdrawUri = stringifyWithdrawUri(uri); + const [show, setShow] = useState(false) + return <Fragment> + + <div class="bg-white shadow sm:rounded-lg mt-4"> + <div class="p-4"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>On this device</i18n.Translate> + </h3> + <div class="mt-2 sm:flex sm:items-start sm:justify-between"> + <div class="max-w-xl text-sm text-gray-500"> + <p> + <i18n.Translate>If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled.</i18n.Translate> + </p> + </div> + <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center"> + <a href={talerWithdrawUri} + 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>Start</i18n.Translate> + </a> + </div> + </div> + </div> + </div> + <div class="bg-white shadow sm:rounded-lg mt-2"> + <div class="p-4"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>On a mobile phone</i18n.Translate> + </h3> + <div class="mt-2 sm:flex sm:items-start sm:justify-between"> + <div class="max-w-xl text-sm text-gray-500"> + <p> + <i18n.Translate>Scan the QR code with your mobile device.</i18n.Translate> + </p> + </div> + <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center"> + <button type="button" + class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500" + onClick={() => { + setShow(!show) + }} + > + {!show ? + <i18n.Translate>Show QR</i18n.Translate> + : + <i18n.Translate>Hide QR</i18n.Translate> + } + </button> + </div> + </div> + {show && + <div class="mt-2 max-w-md ml-auto mr-auto"> + <QR text={talerWithdrawUri} /> + </div> + } + </div> + </div> + + <div class="flex justify-end mt-4"> + <button type="button" + class="inline-flex items-center 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-500" + onClick={() => { + onClose() + }} + > + Cancel + </button> + </div> + </Fragment> } diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 573f8c769..2830f5c1e 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -26,12 +26,11 @@ import { useSettings } from "../hooks/settings.js"; * Let the user choose a payment option, * then specify the details trigger the action. */ -export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { +export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = useSettings(); + const [settings] = useSettings(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); - // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined); return ( <div class="mt-2"> @@ -56,6 +55,14 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { <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> + {!!settings.currentWithdrawalOperationId && + <span class="inline-flex items-center gap-x-1.5 rounded-full bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-700"> + <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true"> + <circle cx="3" cy="3" r="3" /> + </svg> + <i18n.Translate>Operation in progress</i18n.Translate> + </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"> @@ -88,9 +95,7 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode { <WalletWithdrawForm focus limit={limit} - onSuccess={(id) => { - updateSettings("currentWithdrawalOperationId", id); - }} + goToConfirmOperation={goToConfirmOperation} onCancel={() => { setTab(undefined) }} diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 416c714e2..0a5a386ae 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -137,107 +137,3 @@ export function QrCodeSection({ } -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 08f706919..8dbdd9da6 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -36,33 +36,52 @@ 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"; +import { OperationState } from "./OperationState/index.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(Amount); -export function WalletWithdrawForm({ - focus, - limit, - onSuccess, - onCancel, -}: { + +function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { limit: AmountJson; focus?: boolean; - onSuccess: (operationId: string) => void; + goToConfirmOperation: (operationId: string) => void; onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); - const { createWithdrawal } = useAccessAPI(); const [settings, updateSettings] = useSettings() + 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 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 > + } + const trimmedAmountStr = amountStr?.trim(); const parsedAmount = trimmedAmountStr @@ -92,7 +111,8 @@ export function WalletWithdrawForm({ i18n.str`Server responded with an invalid withdraw URI`, i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`); } else { - onSuccess(uri.withdrawalOperationId); + updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + goToConfirmOperation(uri.withdrawalOperationId); } } catch (error) { if (error instanceof RequestError) { @@ -115,113 +135,168 @@ export function WalletWithdrawForm({ } } + return <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4" + 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") + }} + > + 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> + + </form> +} + + +export function WalletWithdrawForm({ + focus, + limit, + onCancel, + goToConfirmOperation, +}: { + limit: AmountJson; + focus?: boolean; + goToConfirmOperation: (operationId: string) => void; + onCancel: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() + return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> <div class="px-4 sm:px-0"> <h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Prepare your wallet</i18n.Translate></h2> <p class="mt-1 text-sm text-gray-500"> - <i18n.Translate>Upon starting you will receive the money in your digital wallet, if you don't have one please <a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">install one from here</a></i18n.Translate>. - </p> - <p class="mt-1 text-sm text-gray-500"> - <i18n.Translate>After using your wallet you will be redirected here to confirm or cancel the operation.</i18n.Translate> + <i18n.Translate>After using your wallet you will confirm or cancel the operation.</i18n.Translate> </p> </div> - {!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); - }} - 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" + <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(); - setAmountStr("50.00") + updateSettings("showInstallWallet", false); }} > - 50.00 + 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> - <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") - }} - > + </p> - 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>} - </form> - : settings.currentWithdrawalOperationId === undefined ? - <Loading /> : - <WithdrawalOperationState - currentOperation={settings.currentWithdrawalOperationId} + {!settings.fastWithdrawal ? + <OldWithdrawalForm + focus={focus} + limit={limit} + onCancel={onCancel} + goToConfirmOperation={goToConfirmOperation} + /> + : + <OperationState currency={limit.currency} - onClose={() => { - onCancel() - }} + onClose={onCancel} + goToConfirmOperation={goToConfirmOperation} /> - } + } + </div> </div> ); } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 9976babdb..25c571e28 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -18,23 +18,17 @@ import { Amounts, HttpStatusCode, Logger, - TranslatedString, WithdrawUriResult, - parsePaytoUri, - parseWithdrawUri, - stringifyWithdrawUri, + parsePaytoUri } from "@gnu-taler/taler-util"; -import { ErrorType, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../components/Loading.js"; -import { useAccessAPI, useWithdrawalDetails } from "../hooks/access.js"; +import { useWithdrawalDetails } from "../hooks/access.js"; import { useSettings } from "../hooks/settings.js"; import { handleNotOkResult } from "./HomePage.js"; -import { QrCodeSection, QrCodeSectionSimpler } from "./QrCodeSection.js"; +import { QrCodeSection } 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"); @@ -54,18 +48,11 @@ export function WithdrawalQRCode({ const [settings, updateSettings] = useSettings(); const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); + 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; @@ -127,22 +114,6 @@ export function WithdrawalQRCode({ </div> </div> </div> - <div class="mt-4"> - <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>Do not show this again</i18n.Translate> - </span> - </span> - <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={() => { - 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> - </div> <div class="mt-5 sm:mt-6"> <button type="button" 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" @@ -182,7 +153,6 @@ export function WithdrawalQRCode({ the exchange is selcted but no account </div> } - return ( <WithdrawalConfirmationQuestion withdrawUri={withdrawUri} @@ -198,126 +168,3 @@ export function WithdrawalQRCode({ /> ); } - - -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/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx index 318a4cfda..f5f77a3ea 100644 --- a/packages/demobank-ui/src/pages/business/Home.tsx +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -360,7 +360,6 @@ function CreateCashout({ type="checkbox" name="asd" onChange={(e): void => { - console.log("asdasd", form.isDebit); form.isDebit = !form.isDebit; updateForm(structuredClone(form)); }} |