summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/OperationState/views.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/OperationState/views.tsx')
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx447
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 &quot;{payto}&quot;</div>;
+}
+export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) {
+ return <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>;
+}
+export function InvalidReserveView({ reserve }: State.InvalidReserve) {
+ return <div>Reserve from server is not valid &quot;{reserve}&quot;</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>
+ );
+}