diff options
Diffstat (limited to 'packages/bank-ui/src/pages/account/ShowAccountDetails.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/account/ShowAccountDetails.tsx | 491 |
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> + ); +} |