diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/OperationState/views.tsx | 403 |
1 files changed, 0 insertions, 403 deletions
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx deleted file mode 100644 index b1f09ba2a..000000000 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ /dev/null @@ -1,403 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { Attention, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import { QR } from "../../components/QR.js"; -import { usePreferences } from "../../hooks/preferences.js"; -import { undefinedIfEmpty } from "../../utils.js"; -import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { State } from "./index.js"; - -export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { - return ( - <div>Payto from server is not valid "{payto}"</div> - ); -} -export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { - return ( - <div>Withdrawal uri from server is not valid "{uri}"</div> - ); -} -export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { - return ( - <div>Reserve from server is not valid "{reserve}"</div> - ); -} - -export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, busy, account }: State.NeedConfirmation) { - const { i18n } = useTranslationContext() - const [settings] = usePreferences() - const [notification, notify, errorHandler] = useLocalNotification() - - 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); - - async function onCancel() { - errorHandler(async () => { - if (!doAbort) return; - const resp = await doAbort() - if (!resp) return; - switch (resp.case) { - case "previously-confirmed": return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "invalid-id": return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "not-found": return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) - } - }) - } - - async function onConfirm() { - errorHandler(async () => { - if (!doConfirm) return; - const hasError = await doConfirm() - if (!hasError) { - if (!settings.showWithdrawalSuccess) { - notifyInfo(i18n.str`Wire transfer completed!`) - } - return - } - switch (hasError.case) { - case "previously-aborted": return notify({ - type: "error", - title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }) - case "no-exchange-or-reserve-selected": return notify({ - type: "error", - title: i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }) - case "invalid-id": return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - case "not-found": return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - case "insufficient-funds": return notify({ - type: "error", - title: i18n.str`Your balance is not enough.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - default: assertUnreachable(hasError) - } - }) - } - - return ( - <div class="bg-white shadow sm:rounded-lg"> - <LocalNotificationBanner notification={notification} /> - <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-3 text-sm leading-6"> - <ShouldBeSameUser username={account}> - <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={(e) => { - e.preventDefault() - 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={!!errors} - onClick={(e) => { - e.preventDefault() - onConfirm() - }} - > - <i18n.Translate>Transfer</i18n.Translate> - </button> - </div> - </form> - </ShouldBeSameUser> - </div> - </div> - </div> - - ); -} -export function FailedView({ error }: State.Failed) { - const { i18n } = useTranslationContext(); - switch (error.case) { - case "unauthorized": return <Attention type="danger" - title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - case "insufficient-funds": return <Attention type="danger" - title={i18n.str`The operation was rejected due to insufficient funds.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - case "account-not-found": return <Attention type="danger" - title={i18n.str`The operation was rejected due to insufficient funds.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - default: assertUnreachable(error) - } -} - -export function AbortedView({ error, onClose }: State.Aborted) { - return ( - <div>aborted</div> - ); -} - -export function ConfirmedView({ error, onClose }: State.Confirmed) { - const { i18n } = useTranslationContext(); - const [settings, updateSettings] = usePreferences() - return ( - <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 confirmed</i18n.Translate> - </h3> - <div class="mt-2"> - <p class="text-sm text-gray-500"> - <i18n.Translate> - The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet. - </i18n.Translate> - </p> - </div> - </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({ uri, onClose: doClose }: State.Ready): VNode<{}> { - const { i18n } = useTranslationContext(); - const [notification, notify, errorHandler] = useLocalNotification() - - const talerWithdrawUri = stringifyWithdrawUri(uri); - 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 meta = document.createElement("meta") - meta.setAttribute("name", "taler-uri") - meta.setAttribute("content", talerWithdrawUri) - document.head.insertBefore(meta, document.head.children.length ? document.head.children[0] : null) - }, []); - - async function onClose() { - errorHandler(async () => { - const hasError = await doClose() - if (!hasError) return; - switch (hasError.case) { - case "previously-confirmed": return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }) - case "invalid-id": return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - case "not-found": return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - default: assertUnreachable(hasError) - } - }) - } - - return <Fragment> - <LocalNotificationBanner notification={notification} /> - - <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> - - <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> - <div class="mt-2 max-w-md ml-auto mr-auto"> - <QR text={talerWithdrawUri} /> - </div> - </div> - </div> - - </Fragment> - -} |