diff options
Diffstat (limited to 'packages/bank-ui/src/pages/account')
3 files changed, 630 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx new file mode 100644 index 000000000..2216b96fc --- /dev/null +++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx @@ -0,0 +1,85 @@ +/* + 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { Cashouts } from "../../components/Cashouts/index.js"; +import { useSessionState } from "../../hooks/session.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; +import { CreateCashout } from "../regional/CreateCashout.js"; +import { RouteDefinition } from "../../route.js"; + +interface Props { + account: string; + routeClose: RouteDefinition; + onAuthorizationRequired: () => void; + routeCashoutDetails: RouteDefinition<{ cid: string }>; + routeMyAccountDetails: RouteDefinition; + routeMyAccountDelete: RouteDefinition; + routeMyAccountPassword: RouteDefinition; + routeMyAccountCashout: RouteDefinition; + routeCreateCashout: RouteDefinition; + routeConversionConfig:RouteDefinition; +} + +export function CashoutListForAccount({ + account, + onAuthorizationRequired, + routeCreateCashout, + routeCashoutDetails, + routeMyAccountCashout, + routeMyAccountDelete, + routeMyAccountDetails, + routeConversionConfig, + routeMyAccountPassword, + routeClose, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const { state: credentials } = useSessionState(); + + const accountIsTheCurrentUser = + credentials.status === "loggedIn" + ? credentials.username === account + : false; + + return ( + <Fragment> + {accountIsTheCurrentUser ? ( + <ProfileNavigation current="cashouts" + routeMyAccountCashout={routeMyAccountCashout} + routeMyAccountDelete={routeMyAccountDelete} + routeMyAccountDetails={routeMyAccountDetails} + routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} + /> + ) : ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Cashout for account {account}</i18n.Translate> + </h1> + )} + + <CreateCashout + focus + routeHere={routeCreateCashout} + routeClose={routeClose} + onAuthorizationRequired={onAuthorizationRequired} + account={account} + /> + + <Cashouts account={account} routeCashoutDetails={routeCashoutDetails} /> + </Fragment> + ); +} 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..62c8df7f8 --- /dev/null +++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -0,0 +1,244 @@ +/* + 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, +} from "@gnu-taler/taler-util"; +import { + Loading, + LocalNotificationBanner, + notifyInfo, + 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 { useBankCoreApiContext } from "../../context/config.js"; +import { useAccountDetails } from "../../hooks/account.js"; +import { useSessionState } from "../../hooks/session.js"; +import { useBankState } from "../../hooks/bank-state.js"; +import { RouteDefinition } from "../../route.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 { bank: api } = 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 api.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, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The username was not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + } + default: + assertUnreachable(resp); + } + } + }); + } + + 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-10 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> + </Fragment> + ); +} diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx new file mode 100644 index 000000000..e21ac2464 --- /dev/null +++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -0,0 +1,301 @@ +/* + 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, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + LocalNotificationBanner, + ShowInputErrorLabel, + notifyInfo, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useSessionState } from "../../hooks/session.js"; +import { useBankState } from "../../hooks/bank-state.js"; +import { RouteDefinition } from "../../route.js"; +import { undefinedIfEmpty } from "../../utils.js"; +import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { ProfileNavigation } from "../ProfileNavigation.js"; + +export function UpdateAccountPassword({ + account: accountName, + routeClose, + onUpdateSuccess, + onAuthorizationRequired, + routeMyAccountCashout, + routeMyAccountDelete, + routeMyAccountDetails, + routeMyAccountPassword, + routeConversionConfig, + focus, + routeHere, +}: { + routeClose: RouteDefinition; + routeHere: RouteDefinition<{ account: string }>; + routeMyAccountDetails: RouteDefinition; + routeMyAccountDelete: RouteDefinition; + routeMyAccountPassword: RouteDefinition; + routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; + focus?: boolean; + onAuthorizationRequired: () => void; + onUpdateSuccess: () => void; + account: string; +}): VNode { + const { i18n } = useTranslationContext(); + const { state: credentials } = useSessionState(); + const token = + credentials.status !== "loggedIn" ? undefined : credentials.token; + const { bank: api } = useBankCoreApiContext(); + + const [current, setCurrent] = useState<string | undefined>(); + const [password, setPassword] = useState<string | undefined>(); + const [repeat, setRepeat] = useState<string | undefined>(); + const [, updateBankState] = useBankState(); + + const accountIsTheCurrentUser = + credentials.status === "loggedIn" + ? credentials.username === accountName + : false; + + const errors = undefinedIfEmpty({ + current: !accountIsTheCurrentUser + ? undefined + : !current + ? i18n.str`Required` + : undefined, + password: !password ? i18n.str`Required` : undefined, + repeat: !repeat + ? i18n.str`Required` + : password !== repeat + ? i18n.str`Repeated password doesn't match` + : undefined, + }); + const [notification, notify, handleError] = useLocalNotification(); + + async function doChangePassword() { + if (!!errors || !password || !token) return; + await handleError(async () => { + const request = { + old_password: current, + new_password: password, + }; + const resp = await api.updatePassword( + { username: accountName, token }, + request, + ); + if (resp.type === "ok") { + notifyInfo(i18n.str`Password changed`); + onUpdateSuccess(); + } else { + switch (resp.case) { + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`Not authorized to change the password, maybe the session is invalid.`, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Account not found`, + }); + case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD: + return notify({ + type: "error", + title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`, + }); + case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD: + return notify({ + type: "error", + title: i18n.str`Your current password doesn't match, can't change to a new password.`, + }); + case HttpStatusCode.Accepted: { + updateBankState("currentChallenge", { + operation: "update-password", + id: String(resp.body.challenge_id), + location: routeHere.url({ account: accountName }), + sent: AbsoluteTime.never(), + request, + }); + return onAuthorizationRequired(); + } + default: + assertUnreachable(resp); + } + } + }); + } + + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + {accountIsTheCurrentUser ? ( + <ProfileNavigation current="credentials" + routeMyAccountCashout={routeMyAccountCashout} + routeMyAccountDelete={routeMyAccountDelete} + routeMyAccountDetails={routeMyAccountDetails} + routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} + /> + ) : ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Account "{accountName}"</i18n.Translate> + </h1> + )} + + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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"> + <i18n.Translate>Update password</i18n.Translate> + </h2> + </div> + <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="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"> + {accountIsTheCurrentUser ? ( + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`Current password`} + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full 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="current" + id="current-password" + data-error={!!errors?.current && current !== undefined} + value={current ?? ""} + onChange={(e) => { + setCurrent(e.currentTarget.value); + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.current} + isDirty={current !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Your current password, for security + </i18n.Translate> + </p> + </div> + ) : undefined} + + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`New password`} + </label> + <div class="mt-2"> + <input + ref={focus ? doAutoFocus : undefined} + type="password" + class="block w-full 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="password" + id="password" + data-error={!!errors?.password && password !== undefined} + value={password ?? ""} + onChange={(e) => { + setPassword(e.currentTarget.value); + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="repeat" + > + {i18n.str`Type it again`} + </label> + <div class="mt-2"> + <input + type="password" + class="block w-full 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="repeat" + id="repeat" + data-error={!!errors?.repeat && repeat !== undefined} + value={repeat ?? ""} + onChange={(e) => { + setRepeat(e.currentTarget.value); + }} + // placeholder="" + autocomplete="off" + /> + <ShowInputErrorLabel + message={errors?.repeat} + isDirty={repeat !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Repeat the same password</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> + <button + type="submit" + name="change" + 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={!!errors} + onClick={(e) => { + e.preventDefault(); + doChangePassword(); + }} + > + <i18n.Translate>Change</i18n.Translate> + </button> + </div> + </form> + </div> + </Fragment> + ); +} |