summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/BankFrame.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/BankFrame.tsx')
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx368
1 files changed, 368 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
new file mode 100644
index 000000000..db757ee07
--- /dev/null
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -0,0 +1,368 @@
+/*
+ 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,
+ Amounts,
+ ObservabilityEventType,
+ TalerError,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ Loading,
+ RouteDefinition,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
+import { useSettingsContext } from "../context/settings.js";
+import { useAccountDetails } from "../hooks/account.js";
+import { useBankState } from "../hooks/bank-state.js";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+export function BankFrame({
+ children,
+ account,
+ routeAccountDetails,
+}: {
+ account?: string;
+ routeAccountDetails?: RouteDefinition;
+ children: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const session = useSessionState();
+ const settings = useSettingsContext();
+ const [preferences, updatePreferences] = usePreferences();
+ const [, , resetBankState] = useBankState();
+
+ const [error, resetError] = useErrorBoundary();
+
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ resetError();
+ }
+ }, [error]);
+
+ return (
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <div class="bg-indigo-600 pb-32">
+ <Header
+ title="Bank"
+ iconLinkURL={settings.iconLinkURL ?? "#"}
+ profileURL={routeAccountDetails?.url({})}
+ notificationURL={
+ preferences.showDebugInfo
+ ? privatePages.notifications.url({})
+ : undefined
+ }
+ onLogout={
+ session.state.status !== "loggedIn"
+ ? undefined
+ : () => {
+ session.logOut();
+ resetBankState();
+ }
+ }
+ sites={
+ !settings.topNavSites ? [] : Object.entries(settings.topNavSites)
+ }
+ supportedLangs={["en", "es", "de"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="pl-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"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`${set} switch`}
+ data-enabled={isOn}
+ 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={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ 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>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+ </div>
+
+ <div class="fixed z-20 top-14 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
+ {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */}
+ </div>
+ </div>
+
+ <main class="-mt-32 flex-1">
+ {account && routeAccountDetails && (
+ <header class="py-6 bg-indigo-600">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <h1 class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <WelcomeAccount
+ account={account}
+ routeAccountDetails={routeAccountDetails}
+ />
+ </span>
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <AccountBalance account={account} />
+ </span>
+ </h1>
+ </div>
+ </header>
+ )}
+
+ <div class="mx-auto max-w-7xl px-4 pb-4 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
+ </div>
+ </div>
+ </main>
+
+ <AppActivity />
+
+ <Footer
+ testingUrlKey="corebank-api-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
+
+function Wait({ class: clazz }: { class?: string }): VNode {
+ return (
+ <Fragment>
+ <style>{`
+ .animated-loader {
+ display: inline-block;
+ --b: 5px;
+ border-radius: 50%;
+ aspect-ratio: 1;
+ padding: 1px;
+ background: conic-gradient(#0000 10%,#4f46e5) content-box;
+ -webkit-mask:
+ repeating-conic-gradient(#0000 0deg,#000 1deg 20deg,#0000 21deg 36deg),
+ radial-gradient(farthest-side,#0000 calc(100% - var(--b) - 1px),#000 calc(100% - var(--b)));
+ -webkit-mask-composite: destination-in;
+ mask-composite: intersect;
+ animation:spinning-loader 1s infinite steps(10);
+ }
+ @keyframes spinning-loader {to{transform: rotate(1turn)}}
+ `}</style>
+ <div class={`animated-loader ${clazz}`} />
+ </Fragment>
+ );
+}
+
+function AppActivity(): VNode {
+ const [lastEvent, setLastEvent] = useState<{
+ url: string;
+ id: string;
+ when: AbsoluteTime;
+ }>();
+ const [status, setStatus] = useState<"ok" | "fail">();
+ const d = useBankCoreApiContext();
+ const onBackendActivity = !d ? undefined : d.onActivity;
+ const cancelRequest = !d ? undefined : d.cancelRequest;
+ const [pref] = usePreferences();
+ useEffect(() => {
+ // console.log("ASDASDS", onBackendActivity)
+ if (!pref.showDebugInfo) return;
+ if (!onBackendActivity) return;
+ return onBackendActivity((ev) => {
+ switch (ev.type) {
+ case ObservabilityEventType.HttpFetchStart: {
+ setLastEvent(ev);
+ setStatus(undefined);
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishError: {
+ setStatus("fail");
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ setStatus("ok");
+ return;
+ }
+ /**
+ * all of this are ignored
+ */
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.TaskReset:
+ case ObservabilityEventType.ShepherdTaskResult:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.Message:
+ return;
+ default: {
+ assertUnreachable(ev);
+ }
+ }
+ });
+ });
+ if (!pref.showDebugInfo || !lastEvent) return <Fragment />;
+ return (
+ <div
+ data-status={status}
+ class="fixed z-20 bottom-0 w-full ease-in-out delay-1000 transition-transform data-[status=ok]:scale-y-0"
+ >
+ <div
+ data-status={status}
+ class="mx-auto w-4/5 center flex p-1 bg-gray-300 m-1 data-[status=fail]:bg-red-200 data-[status=ok]:bg-green-200 "
+ >
+ {!status ? <Wait class="w-6 h-6" /> : <div class="w-6 h-6" />}
+
+ <p class="ml-2 my-auto text-sm text-gray-500">{lastEvent.url}</p>
+ {!status ? (
+ <button
+ onClick={() => {
+ if (cancelRequest) cancelRequest(lastEvent.id);
+ }}
+ >
+ cancel
+ </button>
+ ) : undefined}
+ </div>
+ </div>
+ );
+}
+
+function WelcomeAccount({
+ account,
+ routeAccountDetails,
+}: {
+ account: string;
+ routeAccountDetails: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <div />;
+ }
+ if (result.type === "fail") {
+ return (
+ <a
+ name="account details"
+ href={routeAccountDetails.url({})}
+ class="underline underline-offset-2"
+ >
+ <i18n.Translate>Welcome</i18n.Translate>
+ </a>
+ );
+ }
+ return (
+ <a
+ name="account details"
+ href={routeAccountDetails.url({})}
+ class="underline underline-offset-2"
+ >
+ <i18n.Translate>
+ Welcome, <span class="whitespace-nowrap">{result.body.name}</span>
+ </i18n.Translate>
+ </a>
+ );
+}
+
+function AccountBalance({ account }: { account: string }): VNode {
+ const result = useAccountDetails(account);
+ const { config } = useBankCoreApiContext();
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <div />;
+ }
+ if (result.type === "fail") return <div />;
+
+ return (
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.balance.amount)}
+ negative={result.body.balance.credit_debit_indicator === "debit"}
+ spec={config.currency_specification}
+ />
+ );
+}