summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/account/ShowAccountDetails.tsx')
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx491
1 files changed, 491 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
new file mode 100644
index 000000000..69a186ca1
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -0,0 +1,491 @@
+/*
+ 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,
+ TalerCorebankApi,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri
+} from "@gnu-taler/taler-util";
+import {
+ CopyButton,
+ Loading,
+ LocalNotificationBanner,
+ RouteDefinition,
+ notifyInfo,
+ useBankCoreApiContext,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { useAccountDetails } from "../../hooks/account.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { useSessionState } from "../../hooks/session.js";
+import { LoginForm } from "../LoginForm.js";
+import { ProfileNavigation } from "../ProfileNavigation.js";
+import { AccountForm } from "../admin/AccountForm.js";
+
+export function ShowAccountDetails({
+ account,
+ routeClose,
+ onUpdateSuccess,
+ onAuthorizationRequired,
+ routeMyAccountCashout,
+ routeMyAccountDelete,
+ routeMyAccountDetails,
+ routeHere,
+ routeMyAccountPassword,
+ routeConversionConfig,
+}: {
+ routeClose: RouteDefinition;
+ routeHere: RouteDefinition<{ account: string }>;
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+ onUpdateSuccess: () => void;
+ onAuthorizationRequired: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const {
+ lib: { bank },
+ } = useBankCoreApiContext();
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === account
+ : false;
+
+ const [submitAccount, setSubmitAccount] = useState<
+ TalerCorebankApi.AccountReconfiguration | undefined
+ >();
+ const [notification, notify, handleError] = useLocalNotification();
+ const [, updateBankState] = useBankState();
+
+ const result = useAccountDetails(account);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ async function doUpdate() {
+ if (!submitAccount || !creds) return;
+ await handleError(async () => {
+ const resp = await bank.updateAccount(
+ {
+ token: creds.token,
+ username: account,
+ },
+ submitAccount,
+ );
+
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Account updated`);
+ onUpdateSuccess();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`The rights to change the account are not sufficient`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the legal name, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the debt limit, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
+ return notify({
+ type: "error",
+ title: i18n.str`You can't change the cashout address, please contact the your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
+ type: "error",
+ title: i18n.str`No information for the selected authentication channel.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "update-account",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({ account }),
+ sent: AbsoluteTime.never(),
+ request: submitAccount,
+ });
+ return onAuthorizationRequired();
+ }
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: {
+ return notify({
+ type: "error",
+ title: i18n.str`Authentication channel is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ const url = bank.getRevenueAPI(account);
+ url.username = account;
+ const baseURL = url.href;
+
+ const ac = parsePaytoUri(result.body.payto_uri);
+ const payto = !ac?.isKnown ? undefined : ac;
+ let accountLetter: string | undefined = undefined;
+ if (payto) {
+ switch (payto.targetType) {
+ case "iban": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\niban=${payto.iban}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "x-taler-bank": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naccount=${payto.account}\nhost=${payto.host}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "bitcoin": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naddress=${payto.address}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} showDebug={true} />
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation
+ current="details"
+ routeMyAccountCashout={routeMyAccountCashout}
+ routeMyAccountDelete={routeMyAccountDelete}
+ routeConversionConfig={routeConversionConfig}
+ routeMyAccountDetails={routeMyAccountDetails}
+ routeMyAccountPassword={routeMyAccountPassword}
+ />
+ ) : (
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account "{account}"</i18n.Translate>
+ </h1>
+ )}
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 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 leading-7 text-gray-900">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Change details</i18n.Translate>
+ </span>
+ </span>
+ </div>
+ </h2>
+ </div>
+
+ <AccountForm
+ focus={true}
+ username={account}
+ template={result.body}
+ purpose="update"
+ onChange={(a) => setSubmitAccount(a)}
+ >
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ name="update"
+ 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={!submitAccount}
+ onClick={doUpdate}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </div>
+ </AccountForm>
+ </div>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 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 leading-7 text-gray-900">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Merchant integration</i18n.Translate>
+ </span>
+ </span>
+ </div>
+ </h2>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use this information to link your Taler Merchant Backoffice
+ account with the current bank account. You can start by copying
+ the values, then go to your merchant backoffice service provider,
+ login into your account and look for the "import" button in the
+ "bank account" section.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {payto !== undefined && (
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-type"
+ >
+ {i18n.str`Account type`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-type"
+ id="account-type"
+ disabled={true}
+ value={account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Method to use for wire transfer.
+ </i18n.Translate>
+ </p>
+ </div>
+ {((payto) => {
+ switch (payto.targetType) {
+ case "iban": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={payto.iban}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "x-taler-bank": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={payto.account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "bitcoin": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Address`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={"DE1231231231"}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ }
+ })(payto)}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Owner's name`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={result.body.name}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Legal name of the person holding the account.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Account info URL`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={baseURL}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ From where the merchant can download information about
+ incoming wire transfers to this account.
+ </i18n.Translate>
+ </p>
+ </div>
+ </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">
+ <a
+ href={routeClose.url({})}
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <CopyButton
+ getContent={() => accountLetter ?? ""}
+ class="flex text-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>Copy</i18n.Translate>
+ </CopyButton>
+ </div>
+ </div>
+ )}
+ </div>
+ </Fragment>
+ );
+}