taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 0aee1fcbf037c2c9c202e11a05a99eba00dd0e54
parent 4bc9cc7d7cd075c11ca18293c767ff24ffc2871d
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 29 Oct 2025 16:09:20 -0300

wip merchant

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx | 96++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 247++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx | 76++++++++--------------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx | 13+------------
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 380++++++++++++++++++++++++++++++++-----------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 88++++++++++++++++++++++++-------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx | 47++++++++++++++++++++++++++++++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx | 43+------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 36++++++------------------------------
11 files changed, 456 insertions(+), 655 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -13,74 +13,70 @@ 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 { TranslatedString } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { AccessToken, TranslatedString } from "@gnu-taler/taler-util"; +import { + ButtonBetter, + ButtonBetterBulma, + LocalNotificationBanner, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { useSessionContext } from "../../context/session.js"; export function JumpToElementById({ - testIfExist, onSelect, placeholder, description, }: { placeholder: TranslatedString; description: TranslatedString; - testIfExist: (id: string) => Promise<boolean>; onSelect: (id: string) => void; }): VNode { const { i18n } = useTranslationContext(); - - const [error, setError] = useState<string | undefined>(undefined); - const [id, setId] = useState<string>(); - async function check(currentId: string | undefined): Promise<void> { - if (!currentId) { - setError(i18n.str`Missing ID`); - return; - } - try { - const exi = await testIfExist(currentId); - if (exi) { - onSelect(currentId); - setError(undefined); - } else { - setError(i18n.str`Not found`); - } - } catch { - setError(i18n.str`Not found`); - } - } + + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const checkExist = safeFunctionHandler( + (token: AccessToken, id: string) => + lib.instance.getProductDetails(token, id), + !session.token || !id ? undefined : [session.token, id], + ); + checkExist.onSuccess = (success, t, id) => { + onSelect(id); + }; + checkExist.onFail = () => i18n.str`Not found`; return ( - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <div class="field has-addons"> - <div class="control"> - <input - class={error ? "input is-danger" : "input"} - type="text" - value={id ?? ""} - onChange={(e) => setId(e.currentTarget.value)} - placeholder={placeholder} - /> - {error && ( - <p class="help is-danger" style={{ fontSize: 16 }}> - {error} - </p> - )} + <Fragment> + <LocalNotificationBanner notification={notification} /> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <div class="field has-addons"> + <div class="control"> + <input + class={"input"} + type="text" + value={id ?? ""} + onChange={(e) => setId(e.currentTarget.value)} + placeholder={placeholder} + /> + </div> + <span class="has-tooltip-bottom" data-tooltip={description}> + <ButtonBetterBulma class="button" onClick={checkExist}> + <span class="icon"> + <i class="mdi mdi-arrow-right" /> + </span> + </ButtonBetterBulma> + </span> </div> - <span class="has-tooltip-bottom" data-tooltip={description}> - <button class="button" onClick={() => check(id)}> - <span class="icon"> - <i class="mdi mdi-arrow-right" /> - </span> - </button> - </span> </div> </div> </div> - </div> + </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -226,8 +226,8 @@ export function ClearConfirmModal({ interface CompareAccountsModalProps { onCancel: () => void; confirm: SafeHandlerTemplate<any,any>; - formPayto: Paytos.FullPaytoString | undefined; - testPayto: Paytos.FullPaytoString; + formPayto: Paytos.URI | undefined; + testPayto: Paytos.URI; } function getHostFromHostPath(s: string | undefined) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -20,15 +20,19 @@ */ import { + AccessToken, HttpStatusCode, - PaytoUri, - TalerError, + opEmptySuccess, + Paytos, TalerMerchantApi, - TranslatedString, - assertUnreachable, - parsePaytoUri, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useChallengeHandler, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -41,6 +45,8 @@ import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; +import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; +import { useSessionContext } from "../../../../context/session.js"; import { usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; @@ -51,11 +57,11 @@ type Entity = TalerMerchantApi.AccountAddDetails & { } & TalerForm; interface Props { - onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>; + onCreated: () => void; onBack?: () => void; } -export function CreatePage({ onCreate, onBack }: Props): VNode { +export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [{ developerMode }] = usePreference(); @@ -72,12 +78,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { }); const facadeURL = safeConvertURL(state.credit_facade_url); - const [revenuePayto, setRevenuePayto] = useState<PaytoUri | undefined>( - undefined, - ); - const [testError, setTestError] = useState<TranslatedString | undefined>( + const [revenuePayto, setRevenuePayto] = useState<Paytos.URI | undefined>( undefined, ); + const parsed = !state.payto_uri + ? undefined + : Paytos.fromString(state.payto_uri); + const safeParsed = parsed?.type === "fail" ? undefined : parsed?.body; const errors = undefinedIfEmpty<FormErrors<Entity>>({ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, @@ -112,109 +119,107 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const submitForm = () => { - if (hasErrors) return Promise.reject(); - const credit_facade_url = !state.credit_facade_url + const credit_facade_url = !state.credit_facade_url + ? undefined + : facadeURL?.href; + const credit_facade_credentials: + | TalerMerchantApi.FacadeCredentials + | undefined = + credit_facade_url == undefined ? undefined - : facadeURL?.href; - const credit_facade_credentials: - | TalerMerchantApi.FacadeCredentials - | undefined = - credit_facade_url == undefined - ? undefined - : state.credit_facade_credentials?.type === "basic" - ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } - : { - type: "none", - }; + : state.credit_facade_credentials?.type === "basic" + ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } + : { + type: "none", + }; + const { state: session, lib } = useSessionContext(); - return onCreate({ - payto_uri: state.payto_uri!, - credit_facade_credentials, - credit_facade_url, - }); + const request: TalerMerchantApi.AccountAddDetails = { + payto_uri: state.payto_uri!, + credit_facade_credentials, + credit_facade_url, + }; + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const mfa = useChallengeHandler(); + const add = safeFunctionHandler( + (token: AccessToken, request: Entity, challengeIds: string[]) => + lib.instance.addBankAccount(token, request, { challengeIds }), + !session.token ? undefined : [session.token, request, []], + ); + add.onSuccess = onCreated; + add.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict.`; + } }; - async function testAccountInfo() { - const revenueAPI = !state.credit_facade_url + const repeat = add.lambda((ids: string[]) => [ + add.args![0], + add.args![1], + ids, + ]); + + const revenueAPI = !state.credit_facade_url + ? undefined + : new URL("./", state.credit_facade_url); + + const test = safeFunctionHandler( + testRevenueAPI, + !state.credit_facade_credentials || !revenueAPI ? undefined - : new URL("./", state.credit_facade_url); + : [revenueAPI, state.credit_facade_credentials], + ); - if (revenueAPI) { - const resp = await testRevenueAPI( - revenueAPI, - state.credit_facade_credentials, - ); - if (resp instanceof TalerError) { - setTestError(i18n.str`The request to check the revenue API failed.`); - setState({ - ...state, - verified: undefined, - }); - return; - } else if (resp.type === "fail") { - switch (resp.case) { - case HttpStatusCode.BadRequest: { - setTestError(i18n.str`Server replied with "bad request".`); - setState({ - ...state, - verified: undefined, - }); - return; - } - case HttpStatusCode.Unauthorized: { - setTestError(i18n.str`Unauthorized, check credentials.`); - setState({ - ...state, - verified: undefined, - }); - return; - } - case HttpStatusCode.NotFound: { - setTestError( - i18n.str`The endpoint does not seem to be a Taler Revenue API.`, - ); - setState({ - ...state, - verified: undefined, - }); - return; - } - case TestRevenueErrorType.CANT_VALIDATE: { - setTestError( - i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.`, - ); - setState({ - ...state, - verified: undefined, - }); - return; - } - default: { - assertUnreachable(resp); - } - } - } else { - const found = resp.body; - const match = state.payto_uri === found; - setState({ - ...state, - verified: match, - }); - if (!match) { - setRevenuePayto(parsePaytoUri(resp.body)); - } - setTestError(undefined); - } + test.onSuccess = (success) => { + const match = state.payto_uri === success; + setState({ + ...state, + verified: match, + }); + if (!match) { + const parsed = Paytos.fromString(success); + if (parsed.type === "ok") setRevenuePayto(parsed.body); } - } + }; + + test.onFail = (fail) => { + switch (fail.case) { + case TestRevenueErrorType.CANT_VALIDATE: + return i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.`; + case HttpStatusCode.BadRequest: + return i18n.str`Server didn't like the request we made.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized, check credentials.`; + case HttpStatusCode.NotFound: + return i18n.str`The endpoint does not seem to be a Taler Revenue API.`; + } + }; + if (mfa.pendingChallenge) { + return ( + <SolveMFAChallenges + currentChallenge={mfa.pendingChallenge} + onCompleted={repeat} + onCancel={mfa.doCancelChallenge} + /> + ); + } return ( <Fragment> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -289,26 +294,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { name="verified" readonly threeState - help={ - testError !== undefined - ? testError - : state.verified === undefined - ? i18n.str`Not verified` - : state.verified - ? i18n.str`Last test was successful.` - : i18n.str`Last test failed.` - } side={ - <button + <ButtonBetterBulma class="button is-info" data-tooltip={i18n.str`Compare info from server with account form`} - disabled={!state.credit_facade_url} - onClick={async () => { - await testAccountInfo(); - }} + onClick={test} > <i18n.Translate>Test</i18n.Translate> - </button> + </ButtonBetterBulma> } /> </Fragment> @@ -322,13 +315,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={add} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> @@ -342,16 +334,15 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onCancel={() => { setRevenuePayto(undefined); }} - onConfirm={(d) => { + confirm={safeFunctionHandler(async () => { setState({ ...state, - payto_uri: d, + payto_uri: Paytos.toFullString(revenuePayto), }); setRevenuePayto(undefined); - }} - formPayto={ - !state.payto_uri ? undefined : parsePaytoUri(state.payto_uri) - } + return opEmptySuccess() + },[])} + formPayto={safeParsed} testPayto={revenuePayto} /> )} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -27,14 +27,13 @@ import { OperationFail, OperationOk, PaytoString, + Paytos, TalerError, TalerMerchantApi, TalerRevenueHttpClient, - opFixedSuccess + opFixedSuccess, } from "@gnu-taler/taler-util"; -import type { - HttpRequestLibrary -} from "@gnu-taler/taler-util/http"; +import { readUnexpectedResponseDetails, type HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { BrowserFetchHttpLib, LocalNotificationBannerBulma, @@ -49,6 +48,7 @@ import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; +import { Session } from "inspector"; export type Entity = TalerMerchantApi.AccountAddDetails; interface Props { @@ -57,62 +57,10 @@ interface Props { } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { state, lib } = useSessionContext(); - - - const { i18n } = useTranslationContext(); - - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); - const create = safeFunctionHandler( - (token: AccessToken, request: Entity, challengeIds: string[]) => - lib.instance.addBankAccount(token, request, { challengeIds }), - !state.token ? undefined : [state.token,] - ); - const [doCreate, repeatCreate] = mfa.withMfaHandler( - ({ challengeIds, onChallengeRequired }) => - async function doCreateImpl(request: Entity) { - try { - const resp = await lib.instance.addBankAccount(state.token, request, { - challengeIds, - }); - if (resp.type === "fail") { - if (resp.case === HttpStatusCode.Accepted) { - onChallengeRequired(resp.body); - return; - } - setNotif({ - message: i18n.str`Could not create account`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - onConfirm(); - } catch (error) { - setNotif({ - message: i18n.str`Could not create account`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - } - }, - ); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - onCompleted={repeatCreate} - onCancel={mfa.doCancelChallenge} - /> - ); - } return ( <> - <LocalNotificationBannerBulma notification={notification} /> - <CreatePage onBack={onBack} onCreate={doCreate} /> + <CreatePage onBack={onBack} onCreated={onConfirm} /> </> ); } @@ -126,12 +74,11 @@ export async function testRevenueAPI( revenueAPI: URL, creds: FacadeCredentials | undefined, ): Promise< - | OperationOk<PaytoString> + | OperationOk<Paytos.FullPaytoString> | OperationFail<TestRevenueErrorType.CANT_VALIDATE> | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.Unauthorized> | OperationFail<HttpStatusCode.BadRequest> - | TalerError > { const httpLib: HttpRequestLibrary = new BrowserFetchHttpLib(); const api = new TalerRevenueHttpClient(revenueAPI.href, httpLib); @@ -172,16 +119,9 @@ export async function testRevenueAPI( detail: undefined, }; } - return opFixedSuccess(resp.body.credit_account as PaytoString); + return opFixedSuccess(resp.body.credit_account as Paytos.FullPaytoString); } catch (err) { - if (err instanceof TalerError) { - return err; - // return { - // type: "fail", - // case: TestRevenueErrorType.GENERIC_ERROR, - // detail: err.errorDetail, - // }; - } + // FIXME: should we return some other error code here? throw err; } } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { FormProvider } from "../../../components/form/FormProvider.js"; @@ -28,7 +28,6 @@ import { undefinedIfEmpty } from "../../../utils/table.js"; interface Props { instanceId: string; - hasPassword: boolean | undefined; onNewPassword: (s: string) => void; // onNewPassword: (c: string | undefined, s: string) => void; onBack?: () => void; @@ -36,7 +35,6 @@ interface Props { export function DetailPage({ instanceId, - hasPassword, onBack, onNewPassword, }: Props): VNode { @@ -72,15 +70,6 @@ export function DetailPage({ const text = i18n.str`You are updating the password for the instance with ID "${instanceId}"`; - async function submitForm() { - if (hasErrors) return; - // const oldToken = - // form.old_token !== undefined && hasPassword ? form.old_token : undefined; - const newToken = form.new_token!; - onNewPassword(newToken); - // onNewPassword(oldToken, newToken); - } - return ( <div> <section class="section"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -14,13 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - ChallengeResponse, + AccessToken, HttpStatusCode, MerchantAuthMethod, TalerError, - TalerMerchantManagementHttpClient, - TalerMerchantManagementResultByMethod, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, @@ -29,182 +27,127 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../components/exception/loading.js"; -import { NotificationCard } from "../../../components/menu/index.js"; +import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { useInstanceDetails, useManagedInstanceDetails, } from "../../../hooks/instance.js"; -import { usePreference } from "../../../hooks/preference.js"; -import { Notification } from "../../../utils/types.js"; import { - FOREVER_REFRESHABLE_TOKEN, - LoginPage, - TEMP_TEST_TOKEN, + LoginPage } from "../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; -import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; export interface Props { onChange: () => void; onCancel: () => void; } -export default function PasswordPage(props: Props): VNode { - const { lib, state } = useSessionContext(); +export default function PasswordPage({ onCancel, onChange }: Props): VNode { + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); const result = useInstanceDetails(); - const instanceId = state.instance; - - // const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - - // const [doChangePassword, repeatChangePassword] = mfa.withMfaHandler( - // ({ challengeIds, onChallengeRequired }) => - // async function changePassword( - // // currentPassword: string | undefined, - // newPassword: string, - // ) { - // // if (currentPassword) { - // // const resp = await lib.instance.createAccessToken( - // // instanceId, - // // currentPassword, - // // TEMP_TEST_TOKEN(i18n.str`Testing password`), - // // ); - // // if (resp.case === HttpStatusCode.Accepted) { - // // throw Error("FIXME!!!!"); - // // } - // // if (resp.type !== "ok") { - // // throw Error(resp.detail?.hint ?? "The current password is wrong"); - // // } - // // } + const instanceId = session.instance; - // { - // const resp = await lib.instance.updateCurrentInstanceAuthentication( - // state.token, - // { - // password: newPassword, - // method: MerchantAuthMethod.TOKEN, - // }, - // { challengeIds }, - // ); - // if (resp.type === "fail") { - // if (resp.case === HttpStatusCode.Accepted) { - // onChallengeRequired(resp.body); - // return; - // } - // throw Error(resp.detail?.hint ?? "The request failed"); - // } - // } - - // // const resp = await lib.instance.createAccessToken( - // // instanceId, - // // newPassword, - // // FOREVER_REFRESHABLE_TOKEN(i18n.str`Password changed`), - // // ); - // // if (resp.type === "ok") { - // // logIn(state.instance, resp.body.access_token); - // // return; - // // } else { - // // if (resp.case === HttpStatusCode.Accepted) { - // // throw Error("FIXME!!!!"); - // // } - // // throw Error(resp.detail?.hint ?? "The new login failed"); - // // } - // }, - // ); - - return CommonPassword({ ...props, instanceId }, result, lib.instance); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + const { i18n } = useTranslationContext(); + const mfa = useChallengeHandler(); + + const changePassword = safeFunctionHandler( + (token: AccessToken, id: string, pwd: string, challengeIds: string[]) => + lib.instance.updateCurrentInstanceAuthentication( + token, + { + method: MerchantAuthMethod.TOKEN, + password: pwd, + }, + { challengeIds }, + ), + ); + changePassword.onSuccess = (suc) => { + onChange(); + return i18n.str`Password changed`; + }; + changePassword.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unaouthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + } + }; + + const retry = changePassword.lambda((ids: string[]) => [ + changePassword.args![0], + changePassword.args![2], + changePassword.args![2], + ids, + ]); + if (mfa.pendingChallenge) { + return ( + <SolveMFAChallenges + currentChallenge={mfa.pendingChallenge} + onCompleted={retry} + onCancel={mfa.doCancelChallenge} + /> + ); + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } + } + } + // + return ( + <Fragment> + <LocalNotificationBannerBulma notification={notification} /> + <DetailPage + onBack={onCancel} + instanceId={result.body.name} + onNewPassword={async (newPassword): Promise<void> => { + return ( + !session.token + ? changePassword + : changePassword.withArgs( + session.token, + instanceId, + newPassword, + [], + ) + ).call(); + }} + /> + </Fragment> + ); } -export function AdminPassword(props: Props & { instanceId: string }): VNode { - const { lib, state } = useSessionContext(); - - const subInstanceLib = lib.subInstanceApi(props.instanceId).instance; - const result = useManagedInstanceDetails(props.instanceId); - - const instanceId = props.instanceId; - - // const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - // const mfa = useChallengeHandler(); +export function AdminPassword({ + instanceId, + onCancel, + onChange, +}: Props & { instanceId: string }): VNode { + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); - // const [doChangePassword, repeatChangePassword] = mfa.withMfaHandler( - // ({ challengeIds, onChallengeRequired }) => - // async function changePassword( - // // currentPassword: string | undefined, - // newPassword: string, - // ) { - // // if (currentPassword) { - // // const resp = await lib.instance.createAccessToken( - // // instanceId, - // // currentPassword, - // // TEMP_TEST_TOKEN(i18n.str`Testing password for instance ${instanceId}`), - // // ); - // // if (resp.type !== "ok") { - // // if (resp.case === HttpStatusCode.Accepted) { - // // throw Error("FIXME!!!!"); - // // } - // // throw Error(resp.detail?.hint ?? "The current password is wrong"); - // // } - // // } - - // { - // const resp = await lib.instance.updateInstanceAuthentication( - // state.token, - // props.instanceId, - // { - // password: newPassword, - // method: MerchantAuthMethod.TOKEN, - // }, - // { challengeIds }, - // ); - // if (resp.type === "fail") { - // if (resp.case === HttpStatusCode.Accepted) { - // onChallengeRequired(resp.body); - // return; - // } - // throw Error(resp.detail?.hint ?? "The request failed"); - // } - // } - // // const resp = await subInstanceLib.createAccessToken( - // // instanceId, - // // newPassword, - // // FOREVER_REFRESHABLE_TOKEN( - // // i18n.str`Password changed for instance ${instanceId}`, - // // ), - // // ); - // // if (resp.type === "ok") { - // // return; - // // } else { - // // if (resp.case === HttpStatusCode.Accepted) { - // // throw Error("FIXME!!!!"); - // // } - // // throw Error(resp.detail?.hint ?? "The new login failed"); - // // } - // }, - // ); - - - return CommonPassword(props, result, subInstanceLib); -} - -function CommonPassword( - { onChange, onCancel, instanceId }: Props & { instanceId: string }, - result: - | TalerMerchantManagementResultByMethod<"getInstanceDetails"> - | TalerError - | undefined, - // onNewPassword: ( - // // oldToken: string | undefined, - // newToken: string, - // challengeIds: undefined | string[], - // ) => Promise<void>, - api: TalerMerchantManagementHttpClient, -): VNode { - const { i18n } = useTranslationContext(); - const { state } = useSessionContext(); + const subInstanceLib = lib.subInstanceApi(instanceId).instance; + const result = useManagedInstanceDetails(instanceId); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -224,81 +167,68 @@ function CommonPassword( } } - const adminChangingPwdForAnotherInstance = - state.isAdmin && state.instance !== instanceId; - const hasToken = - result.body.auth.method === MerchantAuthMethod.TOKEN && - !adminChangingPwdForAnotherInstance; - - const id = result.body.name; - - // if (currentPassword) { - // const resp = await lib.instance.createAccessToken( - // instanceId, - // currentPassword, - // TEMP_TEST_TOKEN(i18n.str`Testing password`), - // ); - // if (resp.case === HttpStatusCode.Accepted) { - // throw Error("FIXME!!!!"); - // } - // if (resp.type !== "ok") { - // throw Error(resp.detail?.hint ?? "The current password is wrong"); - // } - // } - - // { - // const resp = await lib.instance.updateCurrentInstanceAuthentication( - // state.token, - // { - // password: newPassword, - // method: MerchantAuthMethod.TOKEN, - // }, - // { challengeIds }, - // ); - // if (resp.type === "fail") { - // if (resp.case === HttpStatusCode.Accepted) { - // onChallengeRequired(resp.body); - // return; - // } - // throw Error(resp.detail?.hint ?? "The request failed"); - // } - // } + const { i18n } = useTranslationContext(); + const mfa = useChallengeHandler(); + const changePassword = safeFunctionHandler( + (token: AccessToken, id: string, pwd: string, challengeIds: string[]) => + lib.instance.updateInstanceAuthentication( + token, + id, + { + method: MerchantAuthMethod.TOKEN, + password: pwd, + }, + { challengeIds }, + ), + ); + changePassword.onSuccess = (suc) => { + onChange(); + return i18n.str`Password changed`; + }; + changePassword.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unaouthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + } + }; + const retry = changePassword.lambda((ids: string[]) => [ + changePassword.args![0], + changePassword.args![2], + changePassword.args![2], + ids, + ]); + if (mfa.pendingChallenge) { + return ( + <SolveMFAChallenges + currentChallenge={mfa.pendingChallenge} + onCompleted={retry} + onCancel={mfa.doCancelChallenge} + /> + ); + } - // const resp = await lib.instance.createAccessToken( - // instanceId, - // newPassword, - // FOREVER_REFRESHABLE_TOKEN(i18n.str`Password changed`), - // ); - // if (resp.type === "ok") { - // logIn(state.instance, resp.body.access_token); - // return; - // } else { - // if (resp.case === HttpStatusCode.Accepted) { - // throw Error("FIXME!!!!"); - // } - // throw Error(resp.detail?.hint ?? "The new login failed"); - // } return ( <Fragment> <LocalNotificationBannerBulma notification={notification} /> <DetailPage onBack={onCancel} instanceId={result.body.name} - hasPassword={hasToken} onNewPassword={async (newPassword): Promise<void> => { - // onNewPassword={async (currentPassword, newPassword): Promise<void> => { - try { - // await onNewPassword(currentPassword, newPassword); - await onNewPassword(newPassword, undefined); - return onChange(); - } catch (error) { - return setNotif({ - message: i18n.str`Failed to set new password`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }); - } + return ( + !session.token + ? changePassword + : changePassword.withArgs( + session.token, + instanceId, + newPassword, + [], + ) + ).call(); }} /> </Fragment> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -19,8 +19,15 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AmountString, + Amounts, + TalerMerchantApi +} from "@gnu-taler/taler-util"; +import { + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { StateUpdater, useState } from "preact/hooks"; @@ -32,11 +39,12 @@ import { } from "../../../../components/form/FormProvider.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; import { dateFormatForSettings, usePreference, } from "../../../../hooks/preference.js"; -import { WithId } from "../../../../declaration.js"; type Entity = TalerMerchantApi.ProductDetail & WithId; @@ -44,10 +52,6 @@ interface Props { instances: Entity[]; onDelete?: (id: Entity) => void; onSelect: (product: Entity) => void; - onUpdate?: ( - id: string, - data: TalerMerchantApi.ProductPatchDetail, - ) => Promise<void>; onCreate?: () => void; selected?: boolean; onLoadMoreBefore?: () => void; @@ -58,7 +62,6 @@ export function CardTable({ instances, onCreate, onSelect, - onUpdate, onDelete, onLoadMoreAfter, onLoadMoreBefore, @@ -99,7 +102,6 @@ export function CardTable({ instances={instances} onSelect={onSelect} onDelete={onDelete} - onUpdate={onUpdate} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} rowSelection={rowSelection} @@ -118,9 +120,6 @@ interface TableProps { rowSelection: string | undefined; instances: Entity[]; onSelect: (id: Entity) => void; - onUpdate: - | ((id: string, data: TalerMerchantApi.ProductPatchDetail) => Promise<void>) - | undefined; onDelete: ((id: Entity) => void) | undefined; rowSelectionHandler: StateUpdater<string | undefined>; onLoadMoreBefore?: () => void; @@ -132,13 +131,45 @@ function Table({ rowSelectionHandler, instances, onSelect, - onUpdate, onDelete, onLoadMoreAfter, onLoadMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); const [preference] = usePreference(); + + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const update = safeFunctionHandler(lib.instance.updateProduct); + update.onSuccess = () => rowSelectionHandler(undefined); + // async (id, prod) => { + // try { + // const resp = await lib.instance.updateProduct( + // state.token, + // id, + // prod, + // ); + // if (resp.type === "ok") { + // setNotif({ + // message: i18n.str`Product updated successfully`, + // type: "SUCCESS", + // }); + // } else { + // setNotif({ + // message: i18n.str`Could not update the product`, + // type: "ERROR", + // description: resp.detail?.hint, + // }); + // } + // } catch (error) { + // setNotif({ + // message: i18n.str`Could not update the product`, + // type: "ERROR", + // description: error instanceof Error ? error.message : undefined, + // }); + // } + // return; + // } return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -211,7 +242,7 @@ function Table({ <tr key="info"> <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -229,7 +260,7 @@ function Table({ class="has-tooltip-right" data-tooltip={i.product_name} onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -239,7 +270,7 @@ function Table({ class="has-tooltip-right" data-tooltip={i.description} onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -249,7 +280,7 @@ function Table({ </td> <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -259,7 +290,7 @@ function Table({ <Fragment> <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -267,7 +298,7 @@ function Table({ </td> <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -279,7 +310,7 @@ function Table({ )} <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -287,7 +318,7 @@ function Table({ </td> <td onClick={() => - onUpdate && rowSelection !== i.id && rowSelectionHandler(i.id) + rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > @@ -332,10 +363,10 @@ function Table({ <FastProductUpdateForm product={i} onUpdate={async (prod) => { - if (!onUpdate) return; - return onUpdate(i.id, prod).then(() => - rowSelectionHandler(undefined), - ); + (!session.token + ? update + : update.withArgs(session.token, i.id, prod) + ).call(); }} onCancel={() => rowSelectionHandler(undefined)} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -25,7 +25,11 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -45,14 +49,32 @@ interface Props { } export default function ProductList({ onCreate, onSelect }: Props): VNode { const result = useInstanceProducts(); - const { state, lib } = useSessionContext(); + const { state: session, lib } = useSessionContext(); const [deleting, setDeleting] = useState< (TalerMerchantApi.ProductDetail & WithId) | null >(null); - + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { i18n } = useTranslationContext(); + const remove = safeFunctionHandler( + lib.instance.deleteProduct, + !session.token || !deleting ? undefined : [session.token, deleting.id], + ); + remove.onSuccess = (suc, t, id) => { + setDeleting(null) + return i18n.str`Product (ID: ${id}) has been deleted` + } + remove.onFail = (fail) =>{ + switch(fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict.`; + } + } if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -76,10 +98,6 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { <LocalNotificationBannerBulma notification={notification} /> <JumpToElementById - testIfExist={async (id) => { - const resp = await lib.instance.getProductDetails(state.token, id); - return resp.type === "ok"; - }} onSelect={onSelect} description={i18n.str`Jump to product with the given product ID`} placeholder={i18n.str`Product id`} @@ -90,34 +108,6 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { onLoadMoreBefore={result.loadFirst} onLoadMoreAfter={result.loadNext} onCreate={onCreate} - onUpdate={async (id, prod) => { - try { - const resp = await lib.instance.updateProduct( - state.token, - id, - prod, - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Product updated successfully`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Could not update the product`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Could not update the product`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - return; - }} onSelect={(product) => onSelect(product.id)} onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) => setDeleting(prod) @@ -131,33 +121,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { danger active onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - const resp = await lib.instance.deleteProduct( - state.token, - deleting.id, - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Could not delete the product`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Could not delete the product`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} + confirm={remove} > <p> <i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -19,8 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AmountString, + HttpStatusCode, + TalerMerchantApi, + TransferInformation, +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -35,16 +45,17 @@ import { URL_REGEX, } from "../../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.TransferInformation; export interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreated: () => void; onBack?: () => void; accounts: string[]; } -export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { +export function CreatePage({ accounts, onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({ @@ -54,6 +65,9 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { credit_amount: `` as AmountString, }); + // <LocalNotificationBannerBulma notification={notification} /> + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); const errors = undefinedIfEmpty<FormErrors<Entity>>({ wtid: !state.wtid ? i18n.str`Required` @@ -70,16 +84,28 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { ? i18n.str`URL doesn't have the right format` : undefined, }); + const data = state as TransferInformation; - const hasErrors = errors !== undefined; - - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as any); + const create = safeFunctionHandler( + lib.instance.informWireTransfer, + !session.token || !!errors ? undefined : [session.token, data], + ); + create.onSuccess = onCreated; + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict.`; + } }; + const hasErrors = errors !== undefined; return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -122,13 +148,12 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={create} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -20,16 +20,8 @@ */ import { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { - LocalNotificationBannerBulma, - useTranslationContext -} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { useSessionContext } from "../../../../context/session.js"; import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; -import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; export type Entity = TalerMerchantApi.TransferInformation; @@ -39,9 +31,6 @@ interface Props { } export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { state, lib } = useSessionContext(); - - const { i18n } = useTranslationContext(); const instance = useInstanceBankAccounts(); const accounts = !instance || instance instanceof TalerError || instance.type === "fail" @@ -50,37 +39,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { return ( <> - <LocalNotificationBannerBulma notification={notification} /> - <CreatePage - onBack={onBack} - accounts={accounts} - onCreate={(request: TalerMerchantApi.TransferInformation) => { - return lib.instance - .informWireTransfer(state.token, request) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Wire transfer informed successfully`, - type: "SUCCESS", - }); - onConfirm() - } else { - setNotif({ - message: i18n.str`Could not inform transfer`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not inform transfer`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} - /> + <CreatePage onBack={onBack} accounts={accounts} onCreated={onConfirm} /> </> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -36,7 +36,7 @@ import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { NotificationCard } from "../../../../components/menu/index.js"; import { Notification } from "../../../../utils/types.js"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; interface Props { onCreate: () => void; @@ -50,7 +50,7 @@ export default function ListTransfer({ onCreate }: Props): VNode { const setFilter = (s?: boolean) => setForm({ ...form, verified: s }); const { i18n } = useTranslationContext(); - const { state, lib } = useSessionContext(); + const { state: session, lib } = useSessionContext(); const [position, setPosition] = useState<string | undefined>(undefined); @@ -81,7 +81,9 @@ export default function ListTransfer({ onCreate }: Props): VNode { }, (id) => setPosition(id), ); - + // <LocalNotificationBannerBulma notification={notification} /> + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const remove = safeFunctionHandler(lib.instance.deleteWireTransfer) if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -111,33 +113,7 @@ export default function ListTransfer({ onCreate }: Props): VNode { onLoadMoreAfter={result.loadNext} onCreate={onCreate} onDelete={async (transfer) => { - try { - const resp = await lib.instance.deleteWireTransfer( - state.token, - String(transfer.transfer_serial_id), - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Wire transfer "${transfer.wtid.substring( - 0, - 16, - )}..." has been deleted`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Failed to delete transfer`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete transfer`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } + (!session.token ? remove : remove.withArgs(session.token, String(transfer.transfer_serial_id))).call() }} onShowAll={() => setFilter(undefined)} onShowUnverified={() => setFilter(false)}