diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 389 |
1 files changed, 265 insertions, 124 deletions
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index cdb612155..ddcd2492d 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -15,26 +15,41 @@ */ import { + AmountJson, + Amounts, HttpStatusCode, Logger, - WithdrawUriResult, + PaytoUri, + PaytoUriIBAN, + PaytoUriTalerBank, + TranslatedString, + WithdrawUriResult } from "@gnu-taler/taler-util"; import { RequestError, + notify, + notifyError, + notifyInfo, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; +import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useAccessAnonAPI } from "../hooks/access.js"; -import { notifyError } from "../hooks/notification.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; -import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; +import { useSettings } from "../hooks/settings.js"; +import { RenderAmount } from "./PaytoWireTransferForm.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); interface Props { onAborted: () => void; withdrawUri: WithdrawUriResult; + details: { + account: PaytoUri, + reserve: string, + amount: AmountJson, + } } /** * Additional authentication required to complete the operation. @@ -42,9 +57,11 @@ interface Props { */ export function WithdrawalConfirmationQuestion({ onAborted, + details, withdrawUri, }: Props): VNode { const { i18n } = useTranslationContext(); + const [settings, updateSettings] = useSettings() const captchaNumbers = useMemo(() => { return { @@ -56,139 +73,263 @@ export function WithdrawalConfirmationQuestion({ const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI(); const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>(); const answer = parseInt(captchaAnswer ?? "", 10); + const [busy, setBusy] = useState<Record<string, undefined>>() 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, - }); + ? 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; + + async function doTransfer() { + try { + setBusy({}) + await confirmWithdrawal( + withdrawUri.withdrawalOperationId, + ); + if (!settings.showWithdrawalSuccess) { + 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) + } + + async function doCancel() { + try { + setBusy({}) + 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 + ) + } + } + setBusy(undefined) + } + return ( <Fragment> - <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1> - <article> - <div class="challenge-div"> - <form - class="challenge-form" - noValidate - onSubmit={(e) => { - e.preventDefault(); - }} - autoCapitalize="none" - autoCorrect="off" - > - <div class="pure-form" id="captcha" name="capcha-form"> - <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2> - <p> - <label for="answer"> - {i18n.str`What is`} - <em> - {captchaNumbers.a} + {captchaNumbers.b} - </em> - ? - </label> - - <input - name="answer" - id="answer" - value={captchaAnswer ?? ""} - type="text" - autoFocus - required - onInput={(e): void => { - setCaptchaAnswer(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.answer} - isDirty={captchaAnswer !== undefined} - /> - </p> - <p> - <button - type="submit" - class="pure-button pure-button-primary btn-confirm" - disabled={!!errors} - onClick={async (e) => { - e.preventDefault(); - try { - await confirmWithdrawal( - withdrawUri.withdrawalOperationId, - ); - } catch (error) { - if (error instanceof RequestError) { - notifyError( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`The withdrawal has been aborted previously and can't be confirmed` - : status === HttpStatusCode.UnprocessableEntity - ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before` - : undefined, - }), - ); - } else { - notifyError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); + <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-3 sm:gap-x-3"> + + <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}> + <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>challenge response test</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>using SMS</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none border-gray-300"> + <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>one time password</i18n.Translate> + </span> + <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>not available</i18n.Translate> + </span> + </span> + </span> + <svg class="h-5 w-5 text-indigo-600 hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + </div> + </div> + <div class="mt-3 text-sm leading-6"> + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</i18n.Translate></h2> + </div> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() + }} + > + <div class="px-4 py-6 sm:p-8"> + <label for="withdraw-amount">{i18n.str`What is`} + <em> + {captchaNumbers.a} + {captchaNumbers.b} + </em> + ? + </label> + <div class="mt-2"> + <div class="relative rounded-md shadow-sm"> + <input + type="text" + // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + aria-describedby="answer" + autoFocus + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={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={doCancel} + > + <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() + doTransfer() + }} + > + <i18n.Translate>Transfer</i18n.Translate> + </button> + </div> + + </form> + </div> + </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> } - } - }} - > - {i18n.str`Confirm`} - </button> - - <button - class="pure-button pure-button-secondary btn-cancel" - onClick={async (e) => { - e.preventDefault(); - try { - await abortWithdrawal(withdrawUri.withdrawalOperationId); - onAborted(); - } catch (error) { - if (error instanceof RequestError) { - notifyError( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Conflict - ? i18n.str`The reserve operation has been confirmed previously and can't be aborted` - : undefined, - }), - ); - } else { - notifyError({ - title: i18n.str`Operation failed, please report`, - description: - error instanceof Error - ? error.message - : JSON.stringify(error), - }); + case "x-taler-bank": { + const p = details.account as PaytoUriTalerBank + const name = p.params["receiver-name"] + return <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd> + </div> + {name && + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd> + </div> + } + </Fragment> } + default: + return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd> + </div> + } - }} - > - {i18n.str`Cancel`} - </button> - </p> + })()} + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + <RenderAmount value={details.amount} /> + </dd> + </div> + </dl> + </div> </div> - </form> - <div class="hint"> - <p> - <i18n.Translate> - A this point, a <b>real</b> bank would ask for an additional - authentication proof (PIN/TAN, one time password, ..), instead - of a simple calculation. - </i18n.Translate> - </p> + </div> </div> - </article> + </div> + </Fragment> ); } |