summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-04-22 11:38:28 -0300
committerSebastian <sebasjm@gmail.com>2024-04-22 11:40:57 -0300
commit82ec30e81e4352146de6e3de668465100ef4274d (patch)
treed9cc3b690560c49915eeb5de57e50bb75a5e0485 /packages
parent66c3a47ec6bb69ed0bda69f53b5242ba4795d823 (diff)
downloadwallet-core-82ec30e81e4352146de6e3de668465100ef4274d.tar.gz
wallet-core-82ec30e81e4352146de6e3de668465100ef4274d.tar.bz2
wallet-core-82ec30e81e4352146de6e3de668465100ef4274d.zip
refactor to keep the challenge status up to date
Diffstat (limited to 'packages')
-rw-r--r--packages/challenger-ui/src/Routing.tsx90
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx (renamed from packages/challenger-ui/src/pages/StartChallenge.tsx)109
-rw-r--r--packages/challenger-ui/src/hooks/session.ts30
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx87
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx138
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts2
6 files changed, 209 insertions, 247 deletions
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
index e1e9434e5..eae182be5 100644
--- a/packages/challenger-ui/src/Routing.tsx
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -15,6 +15,7 @@
*/
import {
+ Loading,
urlPattern,
useCurrentLocation,
useNavigationContext,
@@ -22,14 +23,15 @@ import {
import { Fragment, VNode, h } from "preact";
import { assertUnreachable } from "@gnu-taler/taler-util";
+import { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
+import { SessionId, useSessionState } from "./hooks/session.js";
import { AnswerChallenge } from "./pages/AnswerChallenge.js";
import { AskChallenge } from "./pages/AskChallenge.js";
+import { CallengeCompleted } from "./pages/CallengeCompleted.js";
import { Frame } from "./pages/Frame.js";
import { MissingParams } from "./pages/MissingParams.js";
import { NonceNotFound } from "./pages/NonceNotFound.js";
-import { StartChallenge } from "./pages/StartChallenge.js";
import { Setup } from "./pages/Setup.js";
-import { CallengeCompleted } from "./pages/CallengeCompleted.js";
export function Routing(): VNode {
// check session and defined if this is
@@ -84,6 +86,7 @@ function safeToURL(s: string | undefined): URL | undefined {
function PublicRounting(): VNode {
const location = useCurrentLocation(publicPages);
const { navigateTo } = useNavigationContext();
+ const { start } = useSessionState();
if (location === undefined) {
return <NonceNotFound />;
@@ -95,7 +98,7 @@ function PublicRounting(): VNode {
<Setup
clientId={location.values.client}
onCreated={(nonce) => {
- navigateTo(publicPages.ask.url({ nonce }))
+ navigateTo(publicPages.ask.url({ nonce }));
//response_type=code
//client_id=1
//redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet
@@ -107,7 +110,7 @@ function PublicRounting(): VNode {
case "authorize": {
const responseType = safeGetParam(location.params, "response_type");
const clientId = safeGetParam(location.params, "client_id");
- const redirectURI = safeToURL(
+ const redirectURL = safeToURL(
safeGetParam(location.params, "redirect_uri"),
);
const state = safeGetParam(location.params, "state");
@@ -119,58 +122,107 @@ function PublicRounting(): VNode {
if (
!responseType ||
!clientId ||
- !redirectURI ||
+ !redirectURL ||
!state ||
responseType !== "code"
) {
return <MissingParams />;
}
+ const sessionId: SessionId = {
+ clientId,
+ redirectURL: redirectURL.href,
+ state,
+ };
return (
- <StartChallenge
+ <CheckChallengeIsUpToDate
+ sessionId={sessionId}
nonce={location.values.nonce}
- clientId={clientId}
- redirectURL={redirectURI}
- state={state}
- onSendSuccesful={() => {
+ onCompleted={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onChangeLeft={() => {
+ start(sessionId);
navigateTo(
publicPages.ask.url({
nonce: location.values.nonce,
}),
);
}}
- />
+ onNoMoreChanges={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.ask.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <Loading />
+ </CheckChallengeIsUpToDate>
);
}
case "ask": {
return (
- <AskChallenge
+ <CheckChallengeIsUpToDate
nonce={location.values.nonce}
- onSendSuccesful={() => {
+ onCompleted={() => {
navigateTo(
- publicPages.answer.url({
+ publicPages.completed.url({
nonce: location.values.nonce,
}),
);
}}
- />
+ >
+ <AskChallenge
+ nonce={location.values.nonce}
+ routeSolveChallenge={publicPages.answer}
+ onSendSuccesful={() => {
+ navigateTo(
+ publicPages.answer.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
);
}
case "answer": {
return (
- <AnswerChallenge
+ <CheckChallengeIsUpToDate
nonce={location.values.nonce}
- onComplete={() => {
+ onCompleted={() => {
navigateTo(
publicPages.completed.url({
nonce: location.values.nonce,
}),
);
}}
- />
+ >
+ <AnswerChallenge
+ nonce={location.values.nonce}
+ onComplete={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
);
}
case "completed": {
- return <CallengeCompleted nonce={location.values.nonce} />;
+ return (
+ <CheckChallengeIsUpToDate nonce={location.values.nonce}>
+ <CallengeCompleted nonce={location.values.nonce} />
+ </CheckChallengeIsUpToDate>
+ );
}
default:
assertUnreachable(location);
diff --git a/packages/challenger-ui/src/pages/StartChallenge.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
index 6cf982a3d..04556696b 100644
--- a/packages/challenger-ui/src/pages/StartChallenge.tsx
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -14,71 +14,49 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
Attention,
- Button,
Loading,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useChallengerApiContext,
- useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useChallengeSession } from "../hooks/challenge.js";
-import {
- ChallengerApi,
- HttpStatusCode,
- TalerError,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import { useSessionState } from "../hooks/session.js";
-
-type Form = {
- email: string;
-};
-export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+import { SessionId, useSessionState } from "../hooks/session.js";
-type Props = {
+interface Props {
nonce: string;
- clientId: string;
- redirectURL: URL;
- state: string;
- onSendSuccesful: () => void;
-};
-
-
-export function StartChallenge({
+ children: ComponentChildren;
+ sessionId?: SessionId;
+ onCompleted?: () => void;
+ onChangeLeft?: () => void;
+ onNoMoreChanges?: () => void;
+}
+export function CheckChallengeIsUpToDate({
+ sessionId: sessionFromParam,
nonce,
- clientId,
- redirectURL,
- state,
- onSendSuccesful,
+ children,
+ onCompleted,
+ onChangeLeft,
+ onNoMoreChanges,
}: Props): VNode {
+ const { state, updateStatus } = useSessionState();
const { i18n } = useTranslationContext();
- const { start } = useSessionState();
- const result = useChallengeSession(nonce, {
- clientId,
- redirectURL: redirectURL.href,
- state,
- });
+ const sessionId = sessionFromParam
+ ? sessionFromParam
+ : !state
+ ? undefined
+ : {
+ clientId: state.clientId,
+ redirectURL: state.redirectURL,
+ state: state.state,
+ };
- const session =
- result && !(result instanceof TalerError) && result.type === "ok"
- ? result.body
- : undefined;
-
- useEffect(() => {
- if (session) {
- start({
- clientId,
- redirectURL: redirectURL.href,
- state,
- });
- onSendSuccesful();
- }
- }, [session]);
+ const result = useChallengeSession(nonce, sessionId);
if (!result) {
return <Loading />;
@@ -126,13 +104,22 @@ export function StartChallenge({
}
}
- return <Loading />;
-}
+ updateStatus(result.body);
+
+ if (onCompleted && "redirectURL" in result.body) {
+ onCompleted();
+ return <Loading />;
+ }
+
+ if (onNoMoreChanges && !result.body.changes_left) {
+ onNoMoreChanges();
+ return <Loading />;
+ }
+
+ if (onChangeLeft && !result.body.changes_left) {
+ onChangeLeft();
+ return <Loading />;
+ }
-export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some(
- (k) => (obj as Record<string, T>)[k] !== undefined,
- )
- ? obj
- : undefined;
+ return <Fragment>{children}</Fragment>;
}
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
index 4bb1bfbc8..4d0ffeccf 100644
--- a/packages/challenger-ui/src/hooks/session.ts
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -15,13 +15,14 @@
*/
import {
+ ChallengerApi,
Codec,
buildCodecForObject,
codecForBoolean,
+ codecForChallengeStatus,
codecForNumber,
codecForString,
codecForStringURL,
- codecForURL,
codecOptional,
} from "@gnu-taler/taler-util";
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
@@ -46,6 +47,7 @@ export type LastChallengeResponse = {
export type SessionState = SessionId & {
email: string | undefined;
lastTry: LastChallengeResponse | undefined;
+ challengeStatus: ChallengerApi.ChallengeStatus | undefined;
completedURL: string | undefined;
};
export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
@@ -61,6 +63,7 @@ export const codecForSessionState = (): Codec<SessionState> =>
.property("redirectURL", codecForStringURL())
.property("completedURL", codecOptional(codecForStringURL()))
.property("state", codecForString())
+ .property("challengeStatus", codecOptional(codecForChallengeStatus()))
.property("lastTry", codecOptional(codecForLastChallengeResponse()))
.property("email", codecOptional(codecForString()))
.build("SessionState");
@@ -70,6 +73,7 @@ export interface SessionStateHandler {
start(s: SessionId): void;
accepted(e: string, l: LastChallengeResponse): void;
completed(e: URL): void;
+ updateStatus(s: ChallengerApi.ChallengeStatus): void;
}
const SESSION_STATE_KEY = buildStorageKey(
@@ -92,6 +96,7 @@ export function useSessionState(): SessionStateHandler {
...info,
lastTry: undefined,
completedURL: undefined,
+ challengeStatus: undefined,
email: undefined,
});
cleanAllCache();
@@ -111,6 +116,29 @@ export function useSessionState(): SessionStateHandler {
completedURL: url.href,
});
},
+ updateStatus(st: ChallengerApi.ChallengeStatus) {
+ if (!state) return;
+ if (!state.challengeStatus) {
+ update({
+ ...state,
+ challengeStatus: st,
+ });
+ return;
+ }
+ // current status
+ const cu = state.challengeStatus;
+ if (
+ cu.changes_left !== st.changes_left ||
+ cu.fix_address !== st.fix_address ||
+ cu.last_address !== st.last_address
+ ) {
+ update({
+ ...state,
+ challengeStatus: st,
+ });
+ return;
+ }
+ },
};
}
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index 69600e2ba..bad6d70de 100644
--- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -16,15 +16,16 @@
import {
ChallengerApi,
HttpStatusCode,
- assertUnreachable,
+ assertUnreachable
} from "@gnu-taler/taler-util";
import {
+ Attention,
Button,
LocalNotificationBanner,
ShowInputErrorLabel,
useChallengerApiContext,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -37,13 +38,7 @@ type Props = {
onComplete: () => void;
};
-function SolveChallengeForm({
- nonce,
- onComplete,
-}: {
- nonce: string;
- onComplete: () => void;
-}): VNode {
+export function AnswerChallenge({ nonce, onComplete }: Props): VNode {
const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const { state, accepted, completed } = useSessionState();
@@ -64,8 +59,8 @@ function SolveChallengeForm({
return await lib.bank.challenge(nonce, { email: state.email });
},
(ok) => {
- if ('redirectURL' in ok.body) {
- completed(ok.body.redirectURL)
+ if ("redirectURL" in ok.body) {
+ completed(ok.body.redirectURL);
} else {
accepted(state.email!, {
attemptsLeft: ok.body.attempts_left,
@@ -99,7 +94,7 @@ function SolveChallengeForm({
return lib.bank.solve(nonce, { pin: pin! });
},
(ok) => {
- completed(ok.body.redirectURL as URL)
+ completed(ok.body.redirectURL as URL);
onComplete();
},
(fail) => {
@@ -149,11 +144,13 @@ function SolveChallengeForm({
A TAN was sent to your address &quot;{state.email}&quot;.
</i18n.Translate>
) : (
- <i18n.Translate>
- We recently already sent a TAN to your address &quot;
- {state.email}&quot;. A new TAN will not be transmitted again
- before {state.lastTry.nextSend}.
- </i18n.Translate>
+ <Attention title={i18n.str`Resend failed`} type="warning">
+ <i18n.Translate>
+ We recently already sent a TAN to your address &quot;
+ {state.email}&quot;. A new TAN will not be transmitted again
+ before &quot;{state.lastTry.nextSend}&quot;.
+ </i18n.Translate>
+ </Attention>
)}
</p>
{!lastTryError ? undefined : (
@@ -230,61 +227,7 @@ function SolveChallengeForm({
</form>
</div>
</Fragment>
- );
-}
-
-export function AnswerChallenge({ nonce, onComplete }: Props): VNode {
- const { i18n } = useTranslationContext();
-
- // const result = useChallengeSession(nonce, clientId, redirectURI, state);
-
- // if (!result) {
- // return <Loading />;
- // }
- // if (result instanceof TalerError) {
- // return <div />;
- // }
-
- // if (result.type === "fail") {
- // switch (result.case) {
- // case HttpStatusCode.BadRequest: {
- // return (
- // <Attention type="danger" title={i18n.str`Bad request`}>
- // <i18n.Translate>
- // Could not start the challenge, check configuration.
- // </i18n.Translate>
- // </Attention>
- // );
- // }
- // case HttpStatusCode.NotFound: {
- // return (
- // <Attention type="danger" title={i18n.str`Not found`}>
- // <i18n.Translate>Nonce not found</i18n.Translate>
- // </Attention>
- // );
- // }
- // case HttpStatusCode.NotAcceptable: {
- // return (
- // <Attention type="danger" title={i18n.str`Not acceptable`}>
- // <i18n.Translate>
- // Server has wrong template configuration
- // </i18n.Translate>
- // </Attention>
- // );
- // }
- // case HttpStatusCode.InternalServerError: {
- // return (
- // <Attention type="danger" title={i18n.str`Internal error`}>
- // <i18n.Translate>Check logs</i18n.Translate>
- // </Attention>
- // );
- // }
- // default:
- // assertUnreachable(result);
- // }
- // }
-
- return <SolveChallengeForm nonce={nonce} onComplete={onComplete} />;
+ )
}
export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
index 71f45dde3..675e2b869 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -14,24 +14,20 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ HttpStatusCode
+} from "@gnu-taler/taler-util";
+import {
Attention,
Button,
- Loading,
LocalNotificationBanner,
+ RouteDefinition,
ShowInputErrorLabel,
useChallengerApiContext,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useChallengeSession } from "../hooks/challenge.js";
-import {
- ChallengerApi,
- HttpStatusCode,
- TalerError,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
import { useSessionState } from "../hooks/session.js";
type Form = {
@@ -42,33 +38,29 @@ export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
type Props = {
nonce: string;
onSendSuccesful: () => void;
+ routeSolveChallenge: RouteDefinition<{nonce:string}>,
};
-function ChallengeForm({
- nonce,
- status,
- onSendSuccesful,
-}: {
- nonce: string;
- status: ChallengerApi.ChallengeStatus;
- onSendSuccesful: () => void;
-}): VNode {
- const prevEmail = !status.last_address
- ? undefined
- : ((status.last_address as any)["email"] as string);
- const regexEmail = !status.restrictions
- ? undefined
- : ((status.restrictions as any)["email"] as {
- regex?: string;
- hint?: string;
- hint_i18n?: string;
- });
+export function AskChallenge({ nonce, onSendSuccesful,routeSolveChallenge }: Props): VNode {
+ const { state, accepted, completed } = useSessionState();
+ const status = state?.challengeStatus;
+ const prevEmail =
+ !status || !status.last_address
+ ? undefined
+ : ((status.last_address as any)["email"] as string);
+ const regexEmail =
+ !status || !status.restrictions
+ ? undefined
+ : ((status.restrictions as any)["email"] as {
+ regex?: string;
+ hint?: string;
+ hint_i18n?: string;
+ });
const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [email, setEmail] = useState<string | undefined>(prevEmail);
- const { accepted, completed } = useSessionState();
const [repeat, setRepeat] = useState<string | undefined>();
const errors = undefinedIfEmpty({
@@ -82,10 +74,12 @@ function ChallengeForm({
: undefined
: !EMAIL_REGEX.test(email)
? i18n.str`invalid email`
- : email !== repeat
- ? i18n.str`emails don't match`
- : undefined,
- repeat: !repeat ? i18n.str`required` : undefined,
+ : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : email !== repeat
+ ? i18n.str`emails doesn't match`
+ : undefined,
});
const onSend = withErrorHandler(
@@ -120,6 +114,10 @@ function ChallengeForm({
},
);
+ if (!status) {
+ return <div>no status loaded</div>;
+ }
+
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
@@ -136,6 +134,15 @@ function ChallengeForm({
</i18n.Translate>
</p>
</div>
+ {state.lastTry && (
+ <Fragment>
+ <Attention title={i18n.str`A code has been sent to ${state.email}`}>
+ <i18n.Translate>
+ You can change the destination or <a href={routeSolveChallenge.url({nonce })}><i18n.Translate>complete the challenge here</i18n.Translate></a>.
+ </i18n.Translate>
+ </Attention>
+ </Fragment>
+ )}
<form
method="POST"
class="mx-auto mt-16 max-w-xl sm:mt-20"
@@ -191,6 +198,10 @@ function ChallengeForm({
autocomplete="email"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
</div>
</div>
@@ -217,67 +228,6 @@ function ChallengeForm({
);
}
-export function AskChallenge({ nonce, onSendSuccesful }: Props): VNode {
- const { i18n } = useTranslationContext();
- const { state } = useSessionState();
-
- const result = useChallengeSession(nonce, state);
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <div />;
- }
-
- if (result.type === "fail") {
- switch (result.case) {
- case HttpStatusCode.BadRequest: {
- return (
- <Attention type="danger" title={i18n.str`Bad request`}>
- <i18n.Translate>
- Could not start the challenge, check configuration.
- </i18n.Translate>
- </Attention>
- );
- }
- case HttpStatusCode.NotFound: {
- return (
- <Attention type="danger" title={i18n.str`Not found`}>
- <i18n.Translate>Nonce not found</i18n.Translate>
- </Attention>
- );
- }
- case HttpStatusCode.NotAcceptable: {
- return (
- <Attention type="danger" title={i18n.str`Not acceptable`}>
- <i18n.Translate>
- Server has wrong template configuration
- </i18n.Translate>
- </Attention>
- );
- }
- case HttpStatusCode.InternalServerError: {
- return (
- <Attention type="danger" title={i18n.str`Internal error`}>
- <i18n.Translate>Check logs</i18n.Translate>
- </Attention>
- );
- }
- default:
- assertUnreachable(result);
- }
- }
-
- return (
- <ChallengeForm
- nonce={nonce}
- status={result.body}
- onSendSuccesful={onSendSuccesful}
- />
- );
-}
-
export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
return Object.keys(obj).some(
(k) => (obj as Record<string, T>)[k] !== undefined,
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 3c4b8b587..9c820bb4b 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -59,6 +59,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
const requestTimeout =
options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
const requestCancel = options?.cancellationToken;
+ const requestRedirect = options?.redirect;
const parsedUrl = new URL(requestUrl);
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
@@ -115,6 +116,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
body: myBody,
method: requestMethod,
signal: controller.signal,
+ redirect: requestRedirect
});
if (timeoutId) {