commit 4359c2a9da120afca3901e04baa27403bdc7cafe
parent c405f5e36043699d20f3c45680e0e589a7622bc0
Author: Sebastian <sebasjm@gmail.com>
Date: Fri, 19 Sep 2025 12:31:32 -0300
mfa handler as hook
Diffstat:
12 files changed, 741 insertions(+), 643 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/hooks/challenge.ts b/packages/merchant-backoffice-ui/src/hooks/challenge.ts
@@ -0,0 +1,144 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-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/>
+ */
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ChallengeResponse } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+
+/**
+ * State of the current MFA operation and handler to manage
+ * the state and retry.
+ *
+ */
+interface MfaState<Type extends Array<any>> {
+ /**
+ * If a mfa has been started this will contain
+ * the challenge response.
+ */
+ pendingChallenge: ChallengeResponse | undefined;
+
+ /**
+ * All required challenges has been completed.
+ * The operation should be retried with this
+ * challenges ids.
+ *
+ * @param solvedChallengeIds
+ * @returns
+ */
+ doRetryWithConfimation: (solvedChallengeIds: string[]) => Promise<void>;
+
+ /**
+ * Cancel the current pending challenge.
+ *
+ * @returns
+ */
+ doCancelChallenge: () => void;
+
+ /**
+ * Perform the operation guarded by the MFA handler which
+ * it may start a challenge to be completed.
+ *
+ * It intentionally has the same signature that the original function.
+ * @param arg
+ * @returns
+ */
+ doFirstCall: (...arg: Type) => Promise<void>;
+
+ /**
+ * Perform the operation guarded by the MFA handler which
+ * in the same say that `doFirstCall` but this time for
+ * situation when there are challanges that in the session.
+ *
+ * @param arg
+ * @returns
+ */
+ tryOperation: (challengeIds: string[], ...arg: Type) => Promise<void>;
+}
+
+/**
+ * Handler to be used by the function performing the MFA
+ * guarded operation
+ */
+interface MfaHandler {
+ /**
+ * Callback handler to use when the operation fails with MFA required
+ * @param challenge
+ * @param params
+ * @returns
+ */
+ onChallengeRequired: (challenge: ChallengeResponse, ...params: any[]) => void;
+ /**
+ * Challenges that are already solved and can be used for the operation.
+ * If this is undefined it may mean that it is the first call.
+ */
+ challengeIds: string[] | undefined;
+}
+
+/**
+ * asd
+ */
+type CallbackFactory<T extends any[]> = (
+ h: MfaHandler,
+) => (...args: T) => Promise<void>;
+
+/**
+ * Take a function that may require MFA and return and MfaState
+ * to solve the MFA challenges.
+ *
+ *
+ * @param cf A function that receives MfaHandler with callback and solved challenges and returns a the function to be guarded.
+ * @returns
+ */
+export function useChallengeHandler<T extends any[]>(
+ cf: CallbackFactory<T>,
+): MfaState<T> {
+ const [params, saveParams] = useState<T>();
+ const [current, saveMfaResponse] = useState<ChallengeResponse>();
+
+ async function start(...args: T): Promise<void> {
+ saveParams(args);
+ return run(undefined, ...args)
+ }
+
+ async function run(challengeIds: string[] | undefined, ...args: T): Promise<void> {
+ const guardedOperation = cf({
+ onChallengeRequired: saveMfaResponse,
+ challengeIds,
+ });
+ return guardedOperation(...args);
+ }
+
+ function reset() {
+ saveMfaResponse(undefined);
+ saveParams(undefined);
+ }
+
+ async function retry(newChallengesSolved: string[]): Promise<void> {
+ if (!params) throw Error('calling retry mfa but without starting a challenge')
+ return run(newChallengesSolved, ...params)
+ }
+
+ return {
+ doFirstCall: start,
+ tryOperation: run,
+ doCancelChallenge: reset,
+ doRetryWithConfimation: retry,
+ pendingChallenge: current,
+ };
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
@@ -18,20 +18,20 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
- ChallengeResponse,
HttpStatusCode,
- TalerMerchantApi,
+ InstanceConfigurationMessage,
+ TalerMerchantApi
} from "@gnu-taler/taler-util";
import { 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 { SolveMFAChallenges } from "../../../components/SolveMFA.js";
import { useSessionContext } from "../../../context/session.js";
-import { usePreference } from "../../../hooks/preference.js";
import { Notification } from "../../../utils/types.js";
import { FOREVER_REFRESHABLE_TOKEN } from "../../login/index.js";
import { CreatePage } from "./CreatePage.js";
-import { SolveMFAChallenges } from "../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../hooks/challenge.js";
interface Props {
onBack?: () => void;
@@ -44,63 +44,56 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const { lib, state, logIn } = useSessionContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- | [ChallengeResponse, TalerMerchantApi.InstanceConfigurationMessage]
- | undefined
- >();
-
- async function doCreateImpl(
- d: TalerMerchantApi.InstanceConfigurationMessage,
- challengeIds: string[] | undefined,
- ) {
- if (state.status !== "loggedIn") return;
- try {
- const resp = await lib.instance.createInstance(state.token, d, {
- challengeIds,
- });
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge([resp.body, d]);
- return;
- }
- setNotif({
- message: i18n.str`Failed to create instance`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- return;
- }
- if (d.auth.password) {
- //if auth has been updated, request a new access token
- const result = await lib.instance.createAccessToken(
- d.id,
- d.auth.password,
- FOREVER_REFRESHABLE_TOKEN(i18n.str`Instance created`),
- );
- if (result.type === "ok") {
- const { access_token: token } = result.body;
- logIn(state.instance, token);
+ const mfa = useChallengeHandler<[InstanceConfigurationMessage]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async (d) => {
+ if (state.status !== "loggedIn") return;
+ try {
+ const resp = await lib.instance.createInstance(state.token, d, {
+ challengeIds,
+ });
+ if (resp.type === "fail") {
+ if (resp.case === HttpStatusCode.Accepted) {
+ onChallengeRequired(resp.body);
+ return;
+ }
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ if (d.auth.password) {
+ //if auth has been updated, request a new access token
+ const result = await lib.instance.createAccessToken(
+ d.id,
+ d.auth.password,
+ FOREVER_REFRESHABLE_TOKEN(i18n.str`Instance created`),
+ );
+ if (result.type === "ok") {
+ const { access_token: token } = result.body;
+ logIn(state.instance, token);
+ }
+ }
+ onConfirm();
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
}
- }
- onConfirm();
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to create instance`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- }
- }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => doCreateImpl(currentChallenge[1], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -112,7 +105,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
<CreatePage
onBack={onBack}
forceId={forceId}
- onCreate={(d) => doCreateImpl(d, undefined)}
+ onCreate={mfa.doFirstCall}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
@@ -39,6 +39,7 @@ import { Notification } from "../../../utils/types.js";
import { LoginPage } from "../../login/index.js";
import { View } from "./View.js";
import { SolveMFAChallenges } from "../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../hooks/challenge.js";
interface Props {
onCreate: () => void;
@@ -58,9 +59,6 @@ export default function Instances({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const { state, lib } = useSessionContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- ChallengeResponse | undefined
- >();
if (!result) return <Loading />;
if (result instanceof TalerError) {
@@ -77,59 +75,58 @@ export default function Instances({
}
}
- async function doDeleteImpl(
- challengeIds: undefined | string[],
- ): Promise<void> {
- if (state.status !== "loggedIn") {
- return;
- }
- try {
- const resp = await lib.instance.deleteInstance(
- state.token,
- deleting!.id,
- { challengeIds },
- );
- if (resp.type === "ok") {
- setNotif({
- message: i18n.str`Instance "${deleting!.name}" (ID: ${
- deleting!.id
- }) has been deleted`,
- type: "SUCCESS",
- });
- } else {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge(resp.body);
+ const mfa = useChallengeHandler<[]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doDeleteImpl(): Promise<void> {
+ if (state.status !== "loggedIn") {
return;
}
- setNotif({
- message: i18n.str`Failed to delete instance`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- }
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to delete instance`,
- type: "ERROR",
- description: error instanceof Error ? error.message : undefined,
- });
- // pushNotification({message: 'delete_error', type: 'ERROR' })
- }
- setDeleting(undefined);
+ try {
+ const resp = await lib.instance.deleteInstance(
+ state.token,
+ deleting!.id,
+ { challengeIds },
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Instance "${deleting!.name}" (ID: ${
+ deleting!.id
+ }) has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ if (resp.case === HttpStatusCode.Accepted) {
+ onChallengeRequired(resp.body);
+ return;
+ }
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ // pushNotification({message: 'delete_error', type: 'ERROR' })
+ }
+ setDeleting(undefined);
+ },
+ );
+
+ if (mfa.pendingChallenge) {
+ return (
+ <SolveMFAChallenges
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
+ />
+ );
}
- if (currentChallenge) {
- return (
- <SolveMFAChallenges
- currentChallenge={currentChallenge}
- onCompleted={doDeleteImpl}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
- />
- );
- }
-
return (
<Fragment>
<NotificationCard notification={notif} />
@@ -153,13 +150,13 @@ export default function Instances({
<PurgeModal
element={deleting}
onCancel={() => setDeleting(undefined)}
- onConfirm={() => doDeleteImpl(undefined)}
+ onConfirm={mfa.doFirstCall}
/>
) : (
<DeleteModal
element={deleting}
onCancel={() => setDeleting(undefined)}
- onConfirm={() => doDeleteImpl(undefined)}
+ onConfirm={mfa.doFirstCall}
/>
))}
</Fragment>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx
@@ -34,6 +34,7 @@ import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
import { ConfirmModal, Row } from "../../../../components/modal/index.js";
import { SolveMFAChallenges } from "../../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../../hooks/challenge.js";
export type Entity = TalerMerchantApi.LoginTokenRequest;
interface Props {
@@ -50,57 +51,50 @@ export default function AccessTokenCreatePage({
const { i18n } = useTranslationContext();
const [ok, setOk] = useState<{ token: string; expiration: AbsoluteTime }>();
- const [currentChallenge, setCurrentChallenge] = useState<
- [ChallengeResponse, pwd: string, req: Entity] | undefined
- >();
-
- async function doCreateImpl(
- pwd: string,
- request: Entity,
- challengeIds: string[] | undefined,
- ) {
- try {
- const resp = await lib.instance.createAccessToken(
- state.instance,
- pwd,
- request,
- { challengeIds },
- );
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge([resp.body, pwd, request]);
- return;
+ const mfa = useChallengeHandler<[string, Entity]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doCreateImpl(pwd, request) {
+ try {
+ const resp = await lib.instance.createAccessToken(
+ state.instance,
+ pwd,
+ request,
+ { challengeIds },
+ );
+ if (resp.type === "fail") {
+ if (resp.case === HttpStatusCode.Accepted) {
+ onChallengeRequired(resp.body);
+ return;
+ }
+ setNotif({
+ message: i18n.str`Could not create access token`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ setOk({
+ expiration: AbsoluteTime.fromProtocolTimestamp(
+ resp.body.expiration,
+ ),
+ token: resp.body.access_token,
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not create access token`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
}
- setNotif({
- message: i18n.str`Could not create access token`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- return;
- }
- setOk({
- expiration: AbsoluteTime.fromProtocolTimestamp(resp.body.expiration),
- token: resp.body.access_token,
- });
- } catch (error) {
- setNotif({
- message: i18n.str`Could not create access token`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- }
- }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) =>
- doCreateImpl(currentChallenge[1], currentChallenge[2], ids)
- }
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -152,10 +146,7 @@ export default function AccessTokenCreatePage({
</div>
</ConfirmModal>
)}
- <CreatePage
- onBack={onBack}
- onCreate={(a, b) => doCreateImpl(a, b, undefined)}
- />
+ <CreatePage onBack={onBack} onCreate={mfa.doFirstCall} />
</Fragment>
);
}
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
@@ -48,6 +48,7 @@ import type {
HeadersImpl,
} from "@gnu-taler/taler-util/http";
import { SolveMFAChallenges } from "../../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../../hooks/challenge.js";
export type Entity = TalerMerchantApi.AccountAddDetails;
interface Props {
@@ -60,48 +61,43 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- [ChallengeResponse, Entity] | undefined
- >();
-
- async function doCreateImpl(
- request: Entity,
- challengeIds: string[] | undefined,
- ) {
- try {
- const resp = await lib.instance.addBankAccount(state.token, request, {
- challengeIds,
- });
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge([resp.body, request]);
- return;
+
+ const mfa = useChallengeHandler<[Entity]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doCreateImpl(request) {
+ 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),
+ });
}
- 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 (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => doCreateImpl(currentChallenge[1], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -109,10 +105,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
return (
<>
<NotificationCard notification={notif} />
- <CreatePage
- onBack={onBack}
- onCreate={(e) => doCreateImpl(e, undefined)}
- />
+ <CreatePage onBack={onBack} onCreate={mfa.doFirstCall} />
</>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -20,6 +20,8 @@
*/
import {
+ AccountAddDetails,
+ BankAccountDetail,
ChallengeResponse,
HttpStatusCode,
TalerError,
@@ -40,6 +42,7 @@ import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
import { WithId } from "../../../../declaration.js";
import { SolveMFAChallenges } from "../../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../../hooks/challenge.js";
export type Entity = TalerMerchantApi.AccountPatchDetails & WithId;
@@ -58,9 +61,6 @@ export default function UpdateValidator({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- [ChallengeResponse, TalerMerchantApi.BankAccountDetail, TalerMerchantApi.AccountAddDetails] | undefined
- >();
if (!result) return <Loading />;
if (result instanceof TalerError) {
@@ -103,67 +103,64 @@ export default function UpdateValidator({
});
}
- async function doReplaceImpl(
- prev: TalerMerchantApi.BankAccountDetail,
- next: TalerMerchantApi.AccountAddDetails,
- challengeIds: undefined | string[],
- ) {
- try {
- const resp = await lib.instance.addBankAccount(state.token, next, {
- challengeIds,
- });
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge([resp.body, prev, next]);
+ const mfa = useChallengeHandler<[BankAccountDetail, AccountAddDetails]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doReplaceImpl(prev, next) {
+ try {
+ const resp = await lib.instance.addBankAccount(state.token, next, {
+ 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;
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
return;
}
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- return;
- }
- } catch (error) {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- return;
- }
- try {
- const resp = await lib.instance.deleteBankAccount(
- state.token,
- prev.h_wire,
- );
- if (resp.type === "fail") {
- setNotif({
- message: i18n.str`Could not delete account`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- return;
- }
- } catch (error) {
- setNotif({
- message: i18n.str`Could not delete account`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- return;
- }
- onConfirm();
- }
+ try {
+ const resp = await lib.instance.deleteBankAccount(
+ state.token,
+ prev.h_wire,
+ );
+ if (resp.type === "fail") {
+ setNotif({
+ message: i18n.str`Could not delete account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not delete account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
+ return;
+ }
+ onConfirm();
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => doReplaceImpl(currentChallenge[1], currentChallenge[2], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -175,7 +172,7 @@ export default function UpdateValidator({
account={{ ...result.body, id: bid }}
onBack={onBack}
onUpdate={doUpdateImpl}
- onReplace={(prev, next) => doReplaceImpl(prev, next, undefined)}
+ onReplace={mfa.doFirstCall}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx
@@ -42,6 +42,7 @@ import {
import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
import { SolveMFAChallenges } from "../../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../../hooks/challenge.js";
export interface Props {
onChange: () => void;
@@ -49,160 +50,152 @@ export interface Props {
}
export default function PasswordPage(props: Props): VNode {
- const { lib, state, logIn } = useSessionContext();
+ const { lib, state } = useSessionContext();
const result = useInstanceDetails();
const instanceId = state.instance;
- const { i18n } = useTranslationContext();
- const [settings] = usePreference();
- const [currentChallenge, setCurrentChallenge] = useState<
- [ChallengeResponse, string] | undefined
- >();
+ const mfa = useChallengeHandler<[string]>(
+ ({ 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");
+ // }
+ // }
- async function changePassword(
- // currentPassword: string | undefined,
- newPassword: string,
- challengeIds: undefined | 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 resp = await lib.instance.updateCurrentInstanceAuthentication(
- state.token,
{
- password: newPassword,
- method: MerchantAuthMethod.TOKEN,
- },
- { challengeIds },
- );
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge([resp.body, newPassword]);
- return;
+ 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");
+ }
}
- 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");
- // }
- }
- if (currentChallenge) {
+ // 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");
+ // }
+ },
+ );
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => changePassword(currentChallenge[1], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
- return CommonPassword({ ...props, instanceId }, result, changePassword);
+ return CommonPassword({ ...props, instanceId }, result, mfa.doFirstCall);
}
export function AdminPassword(props: Props & { instanceId: string }): VNode {
const { lib, state } = useSessionContext();
- const { i18n } = useTranslationContext();
- const [settings] = usePreference();
const subInstanceLib = lib.subInstanceApi(props.instanceId).instance;
const result = useManagedInstanceDetails(props.instanceId);
const instanceId = props.instanceId;
- const [currentChallenge, setCurrentChallenge] = useState<
- [ChallengeResponse, string] | undefined
- >();
-
- async function changePassword(
- // currentPassword: string | undefined,
- newPassword: string,
- challengeIds: undefined | 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 mfa = useChallengeHandler<[string]>(
+ ({ 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") {
- 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");
- // }
- }
+ 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");
+ // }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => changePassword(currentChallenge[1], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
- return CommonPassword(props, result, changePassword);
+ return CommonPassword(props, result, mfa.doFirstCall);
}
function CommonPassword(
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx
@@ -35,6 +35,7 @@ import { SolveMFAChallenges } from "../../../components/SolveMFA.js";
import { useSessionContext } from "../../../context/session.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
import { Notification } from "../../../utils/types.js";
+import { useChallengeHandler } from "../../../hooks/challenge.js";
interface Props {
instanceId: string;
@@ -53,9 +54,6 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode {
purge: false,
});
const { i18n } = useTranslationContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- ChallengeResponse | undefined
- >();
const { state: session, lib, logOut } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
@@ -71,66 +69,67 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode {
const text = i18n.str`You are deleting the instance with ID "${instanceId}"`;
- async function doDeleteImpl(challengeIds: undefined | string[]) {
- if (hasErrors) return;
- try {
- const resp = await lib.instance.deleteCurrentInstance(session.token, {
- purge: form.purge,
- challengeIds,
- });
- if (resp.type === "ok") {
- logOut()
- return onDeleted();
- }
- switch (resp.case) {
- case HttpStatusCode.Accepted: {
- setCurrentChallenge(resp.body);
- return;
- }
- case HttpStatusCode.Unauthorized: {
- setNotif({
- message: i18n.str`Failed to delete the instance`,
- type: "ERROR",
- description: resp.detail?.hint,
- });
- return;
- }
- case HttpStatusCode.NotFound: {
- setNotif({
- message: i18n.str`Failed to delete the instance`,
- type: "ERROR",
- description: resp.detail?.hint,
+ const mfa = useChallengeHandler<[]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doDeleteImpl() {
+ if (hasErrors) return;
+ try {
+ const resp = await lib.instance.deleteCurrentInstance(session.token, {
+ purge: form.purge,
+ challengeIds,
});
- return;
- }
- case HttpStatusCode.Conflict: {
+ if (resp.type === "ok") {
+ logOut()
+ return onDeleted();
+ }
+ switch (resp.case) {
+ case HttpStatusCode.Accepted: {
+ onChallengeRequired(resp.body);
+ return;
+ }
+ case HttpStatusCode.Unauthorized: {
+ setNotif({
+ message: i18n.str`Failed to delete the instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Failed to delete the instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ case HttpStatusCode.Conflict: {
+ setNotif({
+ message: i18n.str`Failed to delete the instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ } catch (error) {
setNotif({
- message: i18n.str`Failed to delete the instance`,
+ message: i18n.str`Failed to delete the instance.`,
type: "ERROR",
- description: resp.detail?.hint,
+ description: error instanceof Error ? error.message : undefined,
});
- return;
- }
- default: {
- assertUnreachable(resp);
}
}
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to delete the instance.`,
- type: "ERROR",
- description: error instanceof Error ? error.message : undefined,
- });
- }
- }
- if (currentChallenge) {
+ );
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge}
- onCompleted={doDeleteImpl}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -182,7 +181,7 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode {
? i18n.str`Please complete the marked fields`
: i18n.str`Confirm operation`
}
- onClick={() => doDeleteImpl(undefined)}
+ onClick={mfa.doFirstCall}
>
<i18n.Translate>DELETE</i18n.Translate>
</button>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -39,6 +39,7 @@ import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
import { SolveMFAChallenges } from "../../../components/SolveMFA.js";
import { DeletePage } from "./DeletePage.js";
+import { useChallengeHandler } from "../../../hooks/challenge.js";
export interface Props {
onBack: () => void;
@@ -71,10 +72,6 @@ function CommonUpdate(
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const { state } = useSessionContext();
- const [currentChallenge, setCurrentChallenge] = useState<
- | [ChallengeResponse, TalerMerchantApi.InstanceReconfigurationMessage]
- | undefined
- >();
if (!result) return <Loading />;
if (result instanceof TalerError) {
@@ -94,53 +91,53 @@ function CommonUpdate(
}
}
- async function doUpdateImpl(
- d: TalerMerchantApi.InstanceReconfigurationMessage,
- challengeIds: undefined | string[],
- ) {
- if (state.status !== "loggedIn") {
- return;
- }
- try {
- const resp = await updateInstance(state.token, d, { challengeIds });
- if (resp.type === "ok") {
- return onConfirm();
- }
- switch (resp.case) {
- case HttpStatusCode.Accepted: {
- setCurrentChallenge([resp.body, d]);
+ const mfa = useChallengeHandler<
+ [TalerMerchantApi.InstanceReconfigurationMessage]
+ >(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doUpdateImpl(d) {
+ if (state.status !== "loggedIn") {
return;
}
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.NotFound: {
- setNotif({
+ try {
+ const resp = await updateInstance(state.token, d, { challengeIds });
+ if (resp.type === "ok") {
+ return onConfirm();
+ }
+ switch (resp.case) {
+ case HttpStatusCode.Accepted: {
+ onChallengeRequired(resp.body);
+ return;
+ }
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Failed to update instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ }
+ } catch (error) {
+ return setNotif({
message: i18n.str`Failed to update instance`,
type: "ERROR",
- description: resp.detail?.hint,
+ description: error instanceof Error ? error.message : String(error),
});
}
- }
- } catch (error) {
- return setNotif({
- message: i18n.str`Failed to update instance`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- }
- }
+ },
+ );
+ const [deleting, setDeleting] = useState<boolean>();
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge[0]}
- onCompleted={(ids) => doUpdateImpl(currentChallenge[1], ids)}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
- const [deleting, setDeleting] = useState<boolean>();
return (
<Fragment>
@@ -149,7 +146,7 @@ function CommonUpdate(
onBack={onBack}
isLoading={false}
selected={result.body}
- onUpdate={async (d) => doUpdateImpl(d, undefined)}
+ onUpdate={mfa.doFirstCall}
/>
<div class="columns">
<div class="column" />
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -21,12 +21,11 @@
import {
assertUnreachable,
- ChallengeResponse,
Duration,
HttpStatusCode,
LoginTokenRequest,
LoginTokenScope,
- TranslatedString,
+ TranslatedString
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
@@ -35,8 +34,8 @@ import { AsyncButton } from "../../components/exception/AsyncButton.js";
import { NotificationCard } from "../../components/menu/index.js";
import { SolveMFAChallenges } from "../../components/SolveMFA.js";
import { useSessionContext } from "../../context/session.js";
-import { usePreference } from "../../hooks/preference.js";
import { Notification } from "../../utils/types.js";
+import { useChallengeHandler } from "../../hooks/challenge.js";
interface Props {}
@@ -60,71 +59,68 @@ export function LoginPage(_p: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { state, logIn, getInstanceForUsername, config } = useSessionContext();
const [username, setUsername] = useState(state.instance);
- const [settings] = usePreference();
- const [currentChallenge, setCurrentChallenge] = useState<
- ChallengeResponse | undefined
- >();
const { i18n } = useTranslationContext();
- async function doLoginImpl(challengeIds: string[] | undefined) {
- const api = getInstanceForUsername(username);
+ const mfa = useChallengeHandler(
+ ({ challengeIds, onChallengeRequired }) =>
+ async () => {
+ const api = getInstanceForUsername(username);
- try {
- const result = await api.createAccessToken(
- username,
- password,
- FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`),
- {
- challengeIds,
- },
- );
- if (result.type === "ok") {
- const { access_token: token } = result.body;
- logIn(username, token);
- return;
- } else {
- switch (result.case) {
- case HttpStatusCode.Unauthorized: {
- setNotif({
- message: i18n.str`Your password is incorrect`,
- type: "ERROR",
- });
+ try {
+ const result = await api.createAccessToken(
+ username,
+ password,
+ FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`),
+ {
+ challengeIds,
+ },
+ );
+ if (result.type === "ok") {
+ const { access_token: token } = result.body;
+ logIn(username, token);
return;
+ } else {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ setNotif({
+ message: i18n.str`Your password is incorrect`,
+ type: "ERROR",
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Your instance cannot be found`,
+ type: "ERROR",
+ });
+ return;
+ }
+ case HttpStatusCode.Accepted: {
+ onChallengeRequired(result.body);
+ return;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
- case HttpStatusCode.NotFound: {
- setNotif({
- message: i18n.str`Your instance cannot be found`,
- type: "ERROR",
- });
- return;
- }
- case HttpStatusCode.Accepted: {
- setCurrentChallenge(result.body);
- return;
- }
- default: {
- assertUnreachable(result);
- }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to login.`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
}
- }
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to login.`,
- type: "ERROR",
- description: error instanceof Error ? error.message : undefined,
- });
- }
- }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge}
- onCompleted={doLoginImpl}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -166,7 +162,7 @@ export function LoginPage(_p: Props): VNode {
placeholder={"instance name"}
name="username"
onKeyPress={(e) =>
- e.keyCode === 13 ? doLoginImpl(undefined) : null
+ e.keyCode === 13 ? mfa.doFirstCall() : null
}
value={username}
onInput={(e): void =>
@@ -192,7 +188,7 @@ export function LoginPage(_p: Props): VNode {
placeholder={"current password"}
name="token"
onKeyPress={(e) =>
- e.keyCode === 13 ? doLoginImpl(undefined) : null
+ e.keyCode === 13 ? mfa.doFirstCall() : null
}
value={password}
onInput={(e): void =>
@@ -229,7 +225,7 @@ export function LoginPage(_p: Props): VNode {
)}
<AsyncButton
disabled={!username || !password}
- onClick={() => doLoginImpl(undefined)}
+ onClick={mfa.doFirstCall}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx
@@ -39,6 +39,7 @@ import {
import { NotificationCard } from "../../components/menu/index.js";
import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js";
import { SolveMFAChallenges } from "../../components/SolveMFA.js";
+import { useChallengeHandler } from "../../hooks/challenge.js";
export interface Account {
id: string;
@@ -56,12 +57,8 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
const { i18n } = useTranslationContext();
const { state: session, lib, logIn } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const [currentChallenge, setCurrentChallenge] = useState<
- ChallengeResponse | undefined
- >();
- const [value, setValue] = useState<Partial<Account>>({
- });
+ const [value, setValue] = useState<Partial<Account>>({});
const errors: FormErrors<Account> = {
id: !value.id
? i18n.str`Required`
@@ -102,61 +99,62 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
setValue(v);
}
- async function doCreateImpl(challengeIds: string[] | undefined) {
- try {
- const resp = await lib.instance.createInstanceSelfProvision(
- {
- address: {},
- auth: {
- method: MerchantAuthMethod.TOKEN,
- password: value.password!,
- },
- default_pay_delay: {
- d_us: 1000,
- },
- default_wire_transfer_delay: {
- d_us: 1000,
- },
- id: value.id!,
- jurisdiction: {},
- name: value.name!,
- use_stefan: true,
- email: value.email!,
- phone_number: value.phone,
- },
- { challengeIds },
- );
+ const mfa = useChallengeHandler<[]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doCreateImpl() {
+ try {
+ const resp = await lib.instance.createInstanceSelfProvision(
+ {
+ address: {},
+ auth: {
+ method: MerchantAuthMethod.TOKEN,
+ password: value.password!,
+ },
+ default_pay_delay: {
+ d_us: 1000,
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000,
+ },
+ id: value.id!,
+ jurisdiction: {},
+ name: value.name!,
+ use_stefan: true,
+ email: value.email!,
+ phone_number: value.phone,
+ },
+ { challengeIds },
+ );
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge(resp.body);
- } else {
+ if (resp.type === "fail") {
+ if (resp.case === HttpStatusCode.Accepted) {
+ onChallengeRequired(resp.body);
+ } else {
+ setNotif({
+ message: i18n.str`Failed to create account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ return;
+ }
+ onCreated();
+ } catch (error) {
setNotif({
message: i18n.str`Failed to create account`,
type: "ERROR",
- description: resp.detail?.hint,
+ description: error instanceof Error ? error.message : String(error),
});
}
- return;
- }
- onCreated();
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to create account`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- }
- }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge}
- onCompleted={doCreateImpl}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -238,7 +236,7 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
<AsyncButton
type="is-info"
disabled={!errors}
- onClick={() => doCreateImpl(undefined)}
+ onClick={mfa.doFirstCall}
>
<i18n.Translate>Create</i18n.Translate>
</AsyncButton>
diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx
@@ -33,6 +33,7 @@ import { SolveMFAChallenges } from "../../components/SolveMFA.js";
import { useSessionContext } from "../../context/session.js";
import { Notification } from "../../utils/types.js";
import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js";
+import { useChallengeHandler } from "../../hooks/challenge.js";
interface Form {
password: string;
@@ -56,9 +57,6 @@ export function ResetAccount({
// password: "asd",
// repeat: "asd",
});
- const [currentChallenge, setCurrentChallenge] = useState<
- ChallengeResponse | undefined
- >();
const errors: FormErrors<Form> = {
password: !value.password ? i18n.str`Required` : undefined,
@@ -77,58 +75,60 @@ export function ResetAccount({
};
setValue(v);
}
-
- async function doResetImpl(challengeIds: string[] | undefined) {
- try {
- const resp = await lib.subInstanceApi(instanceId).instance.forgotPasswordSelfProvision(
- {
- method: MerchantAuthMethod.TOKEN,
- password: value.password!,
- },
- {
- challengeIds,
- },
- );
- if (resp.type === "fail") {
- if (resp.case === HttpStatusCode.Accepted) {
- setCurrentChallenge(resp.body);
- } else {
+ const mfa = useChallengeHandler<[]>(
+ ({ challengeIds, onChallengeRequired }) =>
+ async function doResetImpl() {
+ try {
+ const resp = await lib
+ .subInstanceApi(instanceId)
+ .instance.forgotPasswordSelfProvision(
+ {
+ method: MerchantAuthMethod.TOKEN,
+ password: value.password!,
+ },
+ {
+ challengeIds,
+ },
+ );
+ if (resp.type === "fail") {
+ if (resp.case === HttpStatusCode.Accepted) {
+ onChallengeRequired(resp.body);
+ } else {
+ setNotif({
+ message: i18n.str`Failed to create account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ return;
+ }
+ //if auth has been updated, request a new access token
+ const result = await lib.instance.createAccessToken(
+ instanceId,
+ value.password!,
+ FOREVER_REFRESHABLE_TOKEN(i18n.str`Password reset`),
+ );
+ if (result.type === "ok") {
+ const { access_token: token } = result.body;
+ logIn(instanceId, token);
+ }
+ onReseted();
+ } catch (error) {
setNotif({
message: i18n.str`Failed to create account`,
type: "ERROR",
- description: resp.detail?.hint,
+ description: error instanceof Error ? error.message : String(error),
});
}
- return;
- }
- //if auth has been updated, request a new access token
- const result = await lib.instance.createAccessToken(
- instanceId,
- value.password!,
- FOREVER_REFRESHABLE_TOKEN(i18n.str`Password reset`),
- );
- if (result.type === "ok") {
- const { access_token: token } = result.body;
- logIn(instanceId, token);
- }
- onReseted();
- } catch (error) {
- setNotif({
- message: i18n.str`Failed to create account`,
- type: "ERROR",
- description: error instanceof Error ? error.message : String(error),
- });
- }
- }
+ },
+ );
- if (currentChallenge) {
+ if (mfa.pendingChallenge) {
return (
<SolveMFAChallenges
- currentChallenge={currentChallenge}
- onCompleted={doResetImpl}
- onCancel={() => {
- setCurrentChallenge(undefined);
- }}
+ currentChallenge={mfa.pendingChallenge}
+ onCompleted={mfa.doRetryWithConfimation}
+ onCancel={mfa.doCancelChallenge}
/>
);
}
@@ -185,7 +185,7 @@ export function ResetAccount({
</button>
<AsyncButton
disabled={!errors}
- onClick={() => doResetImpl(undefined)}
+ onClick={mfa.doFirstCall}
>
<i18n.Translate>Reset</i18n.Translate>
</AsyncButton>