commit fe400a8ea8e6be287cf2654d9c512301770b2f2f
parent 242669f6ae6599dfc8b1dfe07e2354208ee75b0c
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 4 Jun 2026 13:25:37 -0300
fix #11474
Diffstat:
4 files changed, 250 insertions(+), 402 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -34,7 +34,7 @@ import {
TalerWalletIntegrationBrowserProvider,
ToastBannerBulma,
TranslationProvider,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -100,6 +100,7 @@ import {
buildDefaultBackendBaseURL,
fetchSettings,
} from "./settings.js";
+import { SolveChallengeDialog } from "./components/SolveMFA.js";
const TALER_SCREEN_ID = 2;
const WITH_LOCAL_STORAGE_CACHE = false;
@@ -162,10 +163,9 @@ function SubApp({ baseUrl }: { baseUrl: string }) {
<TalerWalletIntegrationBrowserProvider>
<BrowserHashNavigationProvider>
<CurrenciesProvider>
- <ToastBannerBulma/>
- {/* <ShowNotificationsDialog> */}
- <Routing />
- {/* </ShowNotificationsDialog> */}
+ <ToastBannerBulma />
+ <SolveChallengeDialog />
+ <Routing />
</CurrenciesProvider>
</BrowserHashNavigationProvider>
</TalerWalletIntegrationBrowserProvider>
@@ -176,20 +176,6 @@ function SubApp({ baseUrl }: { baseUrl: string }) {
);
}
-export function ShowNotificationsDialog({
- children,
-}: {
- children: ComponentChildren;
-}): VNode {
- console.log("asdasd")
- return (
- <Fragment>
- <ToastBannerBulma />
- <Fragment key="childs">{children}</Fragment>
- </Fragment>
- );
-}
-
function getInitialBackendBaseURL(
backendFromSettings: string | undefined,
): string {
@@ -242,9 +228,7 @@ function OnConfigError({ children }: { children: ComponentChildren }): VNode {
notification={{
message: i18n.str`Contacting the server failed`,
type: "ERROR",
- description: <Fragment>
- {children}
- </Fragment>
+ description: <Fragment>{children}</Fragment>,
}}
/>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx
@@ -7,9 +7,11 @@ import {
HttpStatusCode,
TalerErrorCode,
TanChannel,
+ TranslatedString,
} from "@gnu-taler/taler-util";
import {
Button,
+ MfaState,
SafeHandler,
undefinedIfEmpty,
useNotificationContext,
@@ -18,7 +20,10 @@ import {
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { useMerchantChallengeHandlerContext } from "../context/challenge.js";
+import {
+ ChallengeState,
+ useMerchantChallengeHandlerContext,
+} from "../context/challenge.js";
import { useSessionContext } from "../context/session.js";
import {
datetimeFormatForPreferences,
@@ -26,15 +31,15 @@ import {
} from "../hooks/preference.js";
import { FormErrors, FormProvider } from "./form/FormProvider.js";
import { InputCode } from "./form/InputCode.js";
+import { ConfirmModal, SimpleModal } from "./modal/index.js";
+import { Loading, Spinner } from "./exception/loading.js";
const TALER_SCREEN_ID = 5;
export interface Props {
- onCompleted: SafeHandler<[challenges: string[]], any>;
onCancel(): void;
- currentChallenge: ChallengeResponse;
focus?: boolean;
- initial?: { request: Challenge; response?: ChallengeRequestResponse };
+ state: ChallengeState;
showFull?: Partial<Record<TanChannel, string>>;
}
@@ -195,7 +200,9 @@ function SolveChallenge({
const verify = actionHandler(
/*verify code*/
- (ct, id, b) => lib.instance.confirmChallenge(id, b),
+ async (ct, id, b) => {
+ return lib.instance.confirmChallenge(id, b);
+ },
!tan ? undefined : ([challenge.challenge_id, { tan }] as const),
);
verify.onSuccess = onSolved;
@@ -272,190 +279,166 @@ function SolveChallenge({
const codeKey = "key" + tries.numTries;
return (
- <Fragment>
- <div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <FormProvider<Form>
- name="settings"
- errors={errors}
- object={value}
- valueHandler={valueHandler}
- >
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">
- <i18n.Translate>Validation code sent.</i18n.Translate>
- </p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <p>
- {(function (): VNode {
- switch (challenge.tan_channel) {
- case TanChannel.SMS:
- if (showFull[TanChannel.SMS]) {
- return (
- <i18n.Translate>
- The verification code sent to the phone "
- <b>{showFull[TanChannel.SMS]}</b>"
- </i18n.Translate>
- );
- }
- return (
- <i18n.Translate>
- The verification code sent to the phone number ending
- with "<b>{challenge.tan_info}</b>"
- </i18n.Translate>
- );
- case TanChannel.EMAIL:
- if (showFull[TanChannel.EMAIL]) {
- return (
- <i18n.Translate>
- The verification code sent to the email address "
- <b>{showFull[TanChannel.EMAIL]}</b>"
- </i18n.Translate>
- );
- }
- return (
- <i18n.Translate>
- The verification code sent to the email address
- starting with "<b>{challenge.tan_info}</b>"
- </i18n.Translate>
- );
- }
- })()}
- </p>
-
- <InputCode<Form>
- name="code"
- key={codeKey}
- label={i18n.str`Verification code`}
- dashesIndex={[3]}
- size={8}
- focus={focus}
- readonly={!tries.solvable || showExpired}
- filter={(c) => {
- const v = Number.parseInt(c, 10);
- if (Number.isNaN(v) || v > 9 || v < 0) return undefined;
- return String(v);
- }}
- />
-
- {showExpired ? (
- <div style={{ display: "flex" }}>
- <p
- class="has-text-danger"
- style={{ alignContent: "center", marginRight: 8 }}
- >
- <i18n.Translate>Code expired.</i18n.Translate>
- </p>
- </div>
- ) : undefined}
- <div>
- <div style={{ display: "flex" }}>
- <p style={{ alignContent: "center", marginRight: 8 }}>
- <i18n.Translate>Didn't received the code?</i18n.Translate>
- </p>
- <Button class="button" onClick={resend}>
- <i18n.Translate>Resend</i18n.Translate>
- </Button>
- </div>
- </div>
- <RetransmissionCodeLimitExpiration
- expiration={currentRetransmission}
- />
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "space-between",
- border: "1px solid",
- borderTop: 0,
- }}
+ <ConfirmModal
+ label={i18n.str`Verify`}
+ description={i18n.str`Validation code sent.`}
+ active
+ onCancel={onCancel}
+ confirm={!tries.numTries ? undefined : verify} // first try is going to be automatically
+ >
+ <FormProvider<Form>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <p>
+ {(function (): VNode {
+ switch (challenge.tan_channel) {
+ case TanChannel.SMS:
+ if (showFull[TanChannel.SMS]) {
+ return (
+ <i18n.Translate>
+ The verification code sent to the phone "
+ <b>{showFull[TanChannel.SMS]}</b>"
+ </i18n.Translate>
+ );
+ }
+ return (
+ <i18n.Translate>
+ The verification code sent to the phone number ending with "
+ <b>{challenge.tan_info}</b>"
+ </i18n.Translate>
+ );
+ case TanChannel.EMAIL:
+ if (showFull[TanChannel.EMAIL]) {
+ return (
+ <i18n.Translate>
+ The verification code sent to the email address "
+ <b>{showFull[TanChannel.EMAIL]}</b>"
+ </i18n.Translate>
+ );
+ }
+ return (
+ <i18n.Translate>
+ The verification code sent to the email address starting
+ with "<b>{challenge.tan_info}</b>"
+ </i18n.Translate>
+ );
+ }
+ })()}
+ </p>
+
+ <InputCode<Form>
+ name="code"
+ key={codeKey}
+ label={i18n.str`Verification code`}
+ dashesIndex={[3]}
+ size={8}
+ focus={focus}
+ readonly={!tries.solvable || showExpired}
+ filter={(c) => {
+ const v = Number.parseInt(c, 10);
+ if (Number.isNaN(v) || v > 9 || v < 0) return undefined;
+ return String(v);
+ }}
+ />
+
+ {showExpired ? (
+ <div style={{ display: "flex" }}>
+ <p
+ class="has-text-danger"
+ style={{ alignContent: "center", marginRight: 8 }}
>
- <button class="button" type="button" onClick={onCancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <Button
- class="button is-success"
- submit
- disabled={!tries.numTries}
- onClick={verify}
- >
- <i18n.Translate>Verify</i18n.Translate>
- </Button>
- </footer>
- </FormProvider>
+ <i18n.Translate>Code expired.</i18n.Translate>
+ </p>
+ </div>
+ ) : undefined}
+ <div>
+ <div style={{ display: "flex" }}>
+ <p style={{ alignContent: "center", marginRight: 8 }}>
+ <i18n.Translate>Didn't received the code?</i18n.Translate>
+ </p>
+ <Button class="button" onClick={resend}>
+ <i18n.Translate>Resend</i18n.Translate>
+ </Button>
+ </div>
</div>
- </div>
- </Fragment>
+ <RetransmissionCodeLimitExpiration expiration={currentRetransmission} />
+ </FormProvider>
+ </ConfirmModal>
);
}
export function SolveChallengeDialog({}: {}): VNode {
const mfa = useMerchantChallengeHandlerContext();
+ const { i18n } = useTranslationContext();
if (!mfa.pending) return <Fragment />;
- return (
- <Fragment>
- <SolveMFAChallenges
- currentChallenge={mfa.pending.requirement}
- initial={mfa.pending.initial}
- focus
- onCompleted={mfa.pending.retry}
- onCancel={mfa.cancel}
- />
- </Fragment>
- );
+ if (mfa.pending.loadingFirstChallenge) {
+ return (
+ <ConfirmModal
+ active
+ noCancelButton
+ description={i18n.str`Loading first challenge...`}
+ label={"asd" as TranslatedString}
+ >
+ <div class="columns is-centered is-vcentered">
+ <Spinner />
+ </div>
+ </ConfirmModal>
+ );
+ }
+ return <SolveMFAChallenges state={mfa.pending} focus onCancel={mfa.cancel} />;
+}
+
+type Active = {
+ ch: Challenge;
+ expiration: AbsoluteTime;
+ retransmission: AbsoluteTime;
+};
+
+function getActiveFromInitial(
+ req: Challenge,
+ res: ChallengeRequestResponse,
+): Active {
+ return {
+ ch: req,
+ expiration: !res.solve_expiration
+ ? AbsoluteTime.never()
+ : AbsoluteTime.fromProtocolTimestamp(res.solve_expiration),
+ retransmission: !res.earliest_retransmission
+ ? AbsoluteTime.never()
+ : AbsoluteTime.fromProtocolTimestamp(res.earliest_retransmission),
+ };
}
function SolveMFAChallenges({
- currentChallenge,
- onCompleted,
onCancel,
- initial,
focus,
+ state,
showFull,
}: Props): VNode {
const { i18n } = useTranslationContext();
const { lib } = useSessionContext();
const [solved, setSolved] = useState<string[]>([]);
+
// FIXME: we should save here also the expiration of the
// tan channel to be used when the user press "i have the code"
+ const { initial, requirement, retry } = state;
- type Active = {
- ch: Challenge;
- expiration: AbsoluteTime;
- retransmission: AbsoluteTime;
- };
const initialActive: Active | undefined =
initial && initial.response
- ? ({
- ch: initial.request,
- expiration: !initial.response.solve_expiration
- ? AbsoluteTime.never()
- : AbsoluteTime.fromProtocolTimestamp(
- initial.response.solve_expiration,
- ),
- retransmission: !initial.response.earliest_retransmission
- ? AbsoluteTime.never()
- : AbsoluteTime.fromProtocolTimestamp(
- initial.response.earliest_retransmission,
- ),
- } as Active)
+ ? getActiveFromInitial(
+ requirement.challenges[initial.currentChallengeIndex],
+ initial.response,
+ )
: undefined;
- // const [retransmission, setRetransmission] = useState(initialRetrans);
-
const [active, setActive] = useState<Active | undefined>(initialActive);
+ const [loadingNext, setLoadingNext] = useState(false);
const defaultSelected =
- currentChallenge.challenges.length > 0
- ? currentChallenge.challenges[0]
- : undefined;
+ requirement.challenges.length > 0 ? requirement.challenges[0] : undefined;
const [selectedChallenge, setSelectedChallenge] = useState(defaultSelected);
const { actionHandler, showError } = useNotificationContext();
@@ -496,8 +479,8 @@ function SolveMFAChallenges({
}
});
- const hasSolvedEnough = currentChallenge.combi_and
- ? solved.length === currentChallenge.challenges.length
+ const hasSolvedEnough = requirement.combi_and
+ ? solved.length === requirement.challenges.length
: solved.length > 0;
const doSend =
@@ -505,6 +488,20 @@ function SolveMFAChallenges({
? sendMessage
: sendMessage.withArgs(selectedChallenge);
+ if (loadingNext) {
+ return (
+ <ConfirmModal
+ active
+ noCancelButton
+ description={i18n.str`Loading next challenge...`}
+ label={"asd" as TranslatedString}
+ >
+ <div class="columns is-centered is-vcentered">
+ <Spinner />
+ </div>
+ </ConfirmModal>
+ );
+ }
if (active) {
return (
<SolveChallenge
@@ -517,22 +514,24 @@ function SolveMFAChallenges({
showFull={showFull ?? {}}
onSolved={async () => {
const total = [...solved, active.ch.challenge_id];
- const enough = currentChallenge.combi_and
- ? total.length === currentChallenge.challenges.length
+ const enough = requirement.combi_and
+ ? total.length === requirement.challenges.length
: total.length > 0;
if (enough) {
setSolved(total);
setActive(undefined);
- await onCompleted.withArgs(total).call();
+ await retry.withArgs(total).call();
} else {
setSolved(total);
- const nextPending = currentChallenge.challenges.find((c) => {
+ const nextPending = requirement.challenges.find((c) => {
const pending = total.indexOf(c.challenge_id) === -1;
return pending;
});
if (nextPending) {
+ setLoadingNext(true)
await sendMessage.withArgs(nextPending).call();
+ setLoadingNext(false)
} else {
setActive(undefined);
}
@@ -542,112 +541,75 @@ function SolveMFAChallenges({
);
}
- const enough = currentChallenge.combi_and
- ? solved.length === currentChallenge.challenges.length
+ const enough = requirement.combi_and
+ ? solved.length === requirement.challenges.length
: solved.length > 0;
return (
- <Fragment>
- <div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">
- <i18n.Translate>
- Multi-factor authentication required.
- </i18n.Translate>
- </p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <div>
- {currentChallenge.combi_and ? (
- <i18n.Translate>
- You must complete all of these requirements.
- </i18n.Translate>
- ) : (
- <i18n.Translate>
- You need to complete at least one of this requirements.
- </i18n.Translate>
- )}
- </div>
- <div class="control" style={{ margin: 8 }}>
- <form>
- {currentChallenge.challenges.map((challenge, idx) => {
- return (
- <label
- class="radio"
- style={{ display: "flex", margin: 0, marginTop: 16 }}
- >
- <div style={{ padding: 4 }}>
- <input
- type="radio"
- name="challenge_id"
- checked={
- selectedChallenge?.challenge_id ===
- challenge.challenge_id
- }
- onChange={() => {
- setSelectedChallenge(challenge);
- }}
- />
- </div>
- <div style={{ alignContent: "center" }}>
- {(function (ch: TanChannel): VNode {
- switch (ch) {
- case TanChannel.SMS:
- return (
- <i18n.Translate>
- An SMS to the phone number ending with{" "}
- <span>{challenge.tan_info}</span>
- </i18n.Translate>
- );
- case TanChannel.EMAIL:
- return (
- <i18n.Translate>
- An email to the address starting with{" "}
- <span>{challenge.tan_info}</span>
- </i18n.Translate>
- );
- }
- })(challenge.tan_channel)}
- </div>
- </label>
- // </section>
- );
- })}
- </form>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "space-between",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <button class="button" type="button" onClick={onCancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <Button class="button is-success" onClick={doSend}>
- <i18n.Translate>Continue</i18n.Translate>
- </Button>
- {!enough ? undefined : (
- <Button
- class="button is-success"
- onClick={onCompleted.withArgs(solved)}
+ <ConfirmModal
+ label={!enough ? i18n.str`Continue` : i18n.str`Complete`}
+ description={i18n.str`Multi-factor authentication required.`}
+ active
+ onCancel={onCancel}
+ confirm={!enough ? doSend : retry.withArgs(solved)}
+ >
+ <div>
+ {requirement.combi_and ? (
+ <i18n.Translate>
+ You must complete all of these requirements.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You need to complete at least one of this requirements.
+ </i18n.Translate>
+ )}
+ </div>
+ <div class="control" style={{ margin: 8 }}>
+ <form>
+ {requirement.challenges.map((challenge, idx) => {
+ return (
+ <label
+ class="radio"
+ style={{ display: "flex", margin: 0, marginTop: 16 }}
>
- <i18n.Translate>Complete</i18n.Translate>
- </Button>
- )}
- </footer>
- </div>
+ <div style={{ padding: 4 }}>
+ <input
+ type="radio"
+ name="challenge_id"
+ checked={
+ selectedChallenge?.challenge_id === challenge.challenge_id
+ }
+ onChange={() => {
+ setSelectedChallenge(challenge);
+ }}
+ />
+ </div>
+ <div style={{ alignContent: "center" }}>
+ {(function (ch: TanChannel): VNode {
+ switch (ch) {
+ case TanChannel.SMS:
+ return (
+ <i18n.Translate>
+ An SMS to the phone number ending with{" "}
+ <span>{challenge.tan_info}</span>
+ </i18n.Translate>
+ );
+ case TanChannel.EMAIL:
+ return (
+ <i18n.Translate>
+ An email to the address starting with{" "}
+ <span>{challenge.tan_info}</span>
+ </i18n.Translate>
+ );
+ }
+ })(challenge.tan_channel)}
+ </div>
+ </label>
+ // </section>
+ );
+ })}
+ </form>
</div>
- </Fragment>
+ </ConfirmModal>
);
}
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -51,6 +51,7 @@ interface Props {
label: TranslatedString;
children?: ComponentChildren;
danger?: boolean;
+ focus?: boolean;
/**
* sometimes we want to prevent the user to close the dialog by error when clicking outside the box
*
@@ -66,6 +67,7 @@ export function ConfirmModal({
confirm,
children,
danger,
+ focus,
label,
noCancelButton,
}: Props): VNode {
@@ -89,7 +91,10 @@ export function ConfirmModal({
</header>
<section class="modal-card-body">{children}</section>
<footer class="modal-card-foot">
- <div class="buttons is-right" style={{ width: "100%" }}>
+ <div
+ class="buttons"
+ style={{ width: "100%", justifyContent: "space-between" }}
+ >
{confirm ? (
<Fragment>
{onCancel && !noCancelButton ? (
@@ -110,7 +115,7 @@ export function ConfirmModal({
<button
type="button"
class="button "
- ref={doAutoFocus}
+ ref={focus ? doAutoFocus : undefined}
onClick={onCancel}
>
<i18n.Translate>Close</i18n.Translate>
@@ -131,51 +136,6 @@ export function ConfirmModal({
);
}
-export function ContinueModal({
- active,
- description,
- onCancel,
- confirm,
- children,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div class={active ? "modal is-active" : "modal"}>
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head has-background-success">
- {!description ? null : <p class="modal-card-title">{description}</p>}
- <button
- type="button"
- class="delete "
- aria-label="close"
- onClick={onCancel}
- />
- </header>
- <section class="modal-card-body">{children}</section>
- <footer class="modal-card-foot">
- <div class="buttons is-right" style={{ width: "100%" }}>
- <Button
- class="button is-success"
- ref={doAutoFocus}
- submit
- onClick={confirm}
- >
- <i18n.Translate>Continue</i18n.Translate>
- </Button>
- </div>
- </footer>
- </div>
- <button
- type="button"
- class="modal-close is-large "
- aria-label="close"
- onClick={onCancel}
- />
- </div>
- );
-}
-
export function SimpleModal({
onCancel,
children,
@@ -200,64 +160,6 @@ export function SimpleModal({
);
}
-export function ClearConfirmModal({
- description,
- onCancel,
- onClear,
- confirm,
- children,
-}: Props & { onClear?: () => void }): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div class="modal is-active">
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card">
- <header class="modal-card-head">
- {!description ? null : <p class="modal-card-title">{description}</p>}
- <button
- type="button"
- class="delete "
- aria-label="close"
- onClick={onCancel}
- />
- </header>
- <section class="modal-card-body is-main-section">{children}</section>
- <footer class="modal-card-foot">
- {onClear && (
- <button
- type="button"
- class="button is-danger"
- onClick={onClear}
- disabled={onClear === undefined}
- >
- <i18n.Translate>Clear</i18n.Translate>
- </button>
- )}
- <div class="buttons is-right" style={{ width: "100%" }}>
- <button class="button " type="button" onClick={onCancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <Button
- class="button is-info"
- submit
- onClick={confirm}
- ref={doAutoFocus}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </Button>
- </div>
- </footer>
- </div>
- <button
- type="button"
- class="modal-close is-large "
- aria-label="close"
- onClick={onCancel}
- />
- </div>
- );
-}
-
interface CompareAccountsModalProps {
onCancel: () => void;
confirm: SafeHandler<any, any>;
diff --git a/packages/merchant-backoffice-ui/src/context/challenge.ts b/packages/merchant-backoffice-ui/src/context/challenge.ts
@@ -33,7 +33,7 @@ import { useContext, useState } from "preact/hooks";
*/
export type ContextType = {
- pending?: MfaState;
+ pending?: ChallengeState;
cancel(): void;
onNewChallenge(
operation: TranslatedString,
@@ -55,12 +55,12 @@ const Context = createContext<ContextType>(initial);
export const useMerchantChallengeHandlerContext = (): ContextType =>
useContext(Context);
-type MfaState = {
+export type ChallengeState = {
requirement: ChallengeResponse;
loadingFirstChallenge: boolean;
title: TranslatedString;
retry: SafeHandler<[string[]], any>;
- initial?: { request: Challenge; response?: ChallengeRequestResponse };
+ initial?: { currentChallengeIndex: number; response?: ChallengeRequestResponse };
};
// FIXME: practically the same as BankChallengeHandlerProvider but we cant
// reuse it because it ask for username
@@ -69,7 +69,7 @@ export const MerchantChallengeHandlerProvider = ({
}: {
children: ComponentChildren;
}): VNode => {
- const [state, setState] = useState<MfaState>();
+ const [state, setState] = useState<ChallengeState>();
const { lib } = useMerchantApiContext();
/**
@@ -113,9 +113,9 @@ export const MerchantChallengeHandlerProvider = ({
title: operation,
retry: handler,
requirement,
- loadingFirstChallenge,
+ loadingFirstChallenge: false,
initial: {
- request: challenge,
+ currentChallengeIndex: 0,
response: result.type === "ok" ? result.body : undefined,
},
});