diff options
Diffstat (limited to 'packages/bank-ui/src/pages/OperationState/views.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/OperationState/views.tsx | 447 |
1 files changed, 447 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx new file mode 100644 index 000000000..62308eca6 --- /dev/null +++ b/packages/bank-ui/src/pages/OperationState/views.tsx @@ -0,0 +1,447 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AbsoluteTime, + HttpStatusCode, + TalerErrorCode, + TranslatedString, + assertUnreachable, + stringifyWithdrawUri, +} from "@gnu-taler/taler-util"; +import { + Attention, + LocalNotificationBanner, + notifyInfo, + useLocalNotification, + useTalerWalletIntegrationAPI, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useEffect } from "preact/hooks"; +import { QR } from "../../components/QR.js"; +import { useBankState } from "../../hooks/bank-state.js"; +import { usePreferences } from "../../hooks/preferences.js"; +import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; +import { State } from "./index.js"; + +export function InvalidPaytoView({ payto }: State.InvalidPayto) { + return <div>Payto from server is not valid "{payto}"</div>; +} +export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) { + return <div>Withdrawal uri from server is not valid "{uri}"</div>; +} +export function InvalidReserveView({ reserve }: State.InvalidReserve) { + return <div>Reserve from server is not valid "{reserve}"</div>; +} + +export function NeedConfirmationView({ + onAbort: doAbort, + onConfirm: doConfirm, + routeHere, + account, + id, + onAuthorizationRequired, +}: State.NeedConfirmation) { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + const [notification, notify, errorHandler] = useLocalNotification(); + const [, updateBankState] = useBankState(); + + async function onCancel() { + errorHandler(async () => { + if (!doAbort) return; + const resp = await doAbort(); + if (!resp) return; + switch (resp.case) { + case HttpStatusCode.Conflict: + 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, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + default: + assertUnreachable(resp); + } + }); + } + + async function onConfirm() { + errorHandler(async () => { + if (!doConfirm) return; + const resp = await doConfirm(); + if (!resp) { + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`); + } + return; + } + switch (resp.case) { + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + return notify({ + type: "error", + title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return notify({ + type: "error", + title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return notify({ + type: "error", + title: i18n.str`Your balance is not enough.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.Accepted: { + updateBankState("currentChallenge", { + operation: "confirm-withdrawal", + id: String(resp.body.challenge_id), + sent: AbsoluteTime.never(), + location: routeHere.url({ wopid: id }), + request: id, + }); + return onAuthorizationRequired(); + } + default: + assertUnreachable(resp); + } + }); + } + + 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="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <button + type="button" + name="cancel" + class="text-sm font-semibold leading-6 text-gray-900" + onClick={(e) => { + e.preventDefault(); + onCancel(); + }} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + <button + type="submit" + name="transfer" + 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" + 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 HttpStatusCode.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 HttpStatusCode.Conflict: + 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 HttpStatusCode.NotFound: + 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() { + return <div>aborted</div>; +} + +export function ConfirmedView({ routeClose }: 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" + name="toggle withdrawal" + 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"> + <a + href={routeClose.url({})} + type="button" + name="close" + 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" + > + <i18n.Translate>Close</i18n.Translate> + </a> + </div> + </Fragment> + ); +} + +export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode { + const { i18n } = useTranslationContext(); + const walletInegrationApi = useTalerWalletIntegrationAPI(); + const [notification, notify, errorHandler] = useLocalNotification(); + + const talerWithdrawUri = stringifyWithdrawUri(uri); + useEffect(() => { + walletInegrationApi.publishTalerAction(uri); + }, []); + + async function onAbort() { + errorHandler(async () => { + const hasError = await doAbort(); + if (!hasError) return; + switch (hasError.case) { + case HttpStatusCode.Conflict: + 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, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + when: AbsoluteTime.now(), + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + when: AbsoluteTime.now(), + }); + default: + assertUnreachable(hasError); + } + }); + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + + <div class="flex justify-end mt-4"> + <button + type="button" + name="cancel" + 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={onAbort} + > + <i18n.Translate>Cancel</i18n.Translate> + </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 web browser on desktop you can also + </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} + name="start" + 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> + ); +} |