aboutsummaryrefslogtreecommitdiff
path: root/packages/challenger-ui/src/pages/AnswerChallenge.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/challenger-ui/src/pages/AnswerChallenge.tsx')
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx315
1 files changed, 216 insertions, 99 deletions
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index 5fe2d9743..13ae16a33 100644
--- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -14,8 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- ChallengerApi,
+ AbsoluteTime,
+ EmptyObject,
HttpStatusCode,
+ TalerError,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
@@ -24,128 +26,269 @@ import {
LocalNotificationBanner,
RouteDefinition,
ShowInputErrorLabel,
+ Time,
useChallengerApiContext,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
+import {
+ revalidateChallengeSession,
+ useChallengeSession,
+} from "../hooks/challenge.js";
import { useSessionState } from "../hooks/session.js";
-export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
-
type Props = {
- nonce: string;
focus?: boolean;
onComplete: () => void;
- routeAsk: RouteDefinition<{ nonce: string }>;
+ routeAsk: RouteDefinition<EmptyObject>;
};
-export function AnswerChallenge({
- focus,
- nonce,
- onComplete,
- routeAsk,
-}: Props): VNode {
- const { lib } = useChallengerApiContext();
+function useReloadOnDeadline(deadline: AbsoluteTime): void {
+ const [, set] = useState(false);
+ function toggle(): void {
+ set((s) => !s);
+ }
+ useEffect(() => {
+ if (AbsoluteTime.isExpired(deadline)) {
+ return;
+ }
+ const diff = AbsoluteTime.difference(AbsoluteTime.now(), deadline);
+ if (diff.d_ms === "forever") return;
+ const timer = setTimeout(toggle, diff.d_ms);
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [deadline]);
+}
+
+export function AnswerChallenge({ focus, onComplete, routeAsk }: Props): VNode {
+ const { config, lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
- const { state, accepted, completed } = useSessionState();
+ const { state, sent, failed, completed } = useSessionState();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [pin, setPin] = useState<string | undefined>();
- const [lastTryError, setLastTryError] =
- useState<ChallengerApi.InvalidPinResponse>();
const errors = undefinedIfEmpty({
pin: !pin ? i18n.str`Can't be empty` : undefined,
});
- const lastEmail = !state
+ const restrictionKeys = !config.restrictions
+ ? []
+ : Object.keys(config.restrictions);
+ const restrictionKey = !restrictionKeys.length
? undefined
- : !state.lastStatus
+ : restrictionKeys[0];
+
+ const result = useChallengeSession(state);
+
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ const deadline =
+ lastStatus == undefined
? undefined
- : !state.lastStatus.last_address
- ? undefined
- : state.lastStatus.last_address["email"];
+ : AbsoluteTime.fromProtocolTimestamp(lastStatus.retransmission_time);
+
+ useReloadOnDeadline(deadline ?? AbsoluteTime.never());
+
+ if (!restrictionKey) {
+ return (
+ <div>
+ invalid server configuration, there is no restriction in /config
+ </div>
+ );
+ }
+
+ const lastAddr = !lastStatus?.last_address
+ ? undefined
+ : lastStatus.last_address[restrictionKey];
+
+ const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1;
+ const contact = lastAddr ? { [restrictionKey]: lastAddr } : undefined;
const onSendAgain =
- !state || lastEmail === undefined
+ !state?.nonce ||
+ contact === undefined ||
+ lastStatus == undefined ||
+ lastStatus.pin_transmissions_left === 0 ||
+ !deadline ||
+ !AbsoluteTime.isExpired(deadline)
? undefined
: withErrorHandler(
async () => {
- if (!lastEmail) return;
- return await lib.challenger.challenge(nonce, { email: lastEmail });
+ return await lib.challenger.challenge(state.nonce, contact);
},
(ok) => {
if (ok.body.type === "completed") {
- completed(new URL(ok.body.redirect_url));
+ completed(ok.body);
} else {
- accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
- transmitted: ok.body.transmitted,
- });
+ sent(ok.body);
}
- return undefined;
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str``;
- case HttpStatusCode.Forbidden:
- return i18n.str``;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
}
},
);
const onCheck =
- errors !== undefined || (lastTryError && lastTryError.exhausted)
+ !state?.nonce ||
+ errors !== undefined ||
+ lastStatus == undefined ||
+ lastStatus.auth_attempts_left === 0
? undefined
: withErrorHandler(
async () => {
- return lib.challenger.solve(nonce, { pin: pin! });
+ return lib.challenger.solve(state.nonce, { pin: pin! });
},
(ok) => {
if (ok.body.type === "completed") {
- completed(new URL(ok.body.redirect_url));
+ completed(ok.body);
} else {
- setLastTryError(ok.body);
+ failed(ok.body);
}
onComplete();
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str`Invalid request`;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.Forbidden: {
- return i18n.str`Too many attemps where made`;
+ revalidateChallengeSession();
+ return i18n.str`Invalid pin.`;
}
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
- case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
+ case HttpStatusCode.TooManyRequests: {
+ revalidateChallengeSession();
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
+ }
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
default:
assertUnreachable(fail);
}
},
);
+ const cantTryAnymore = lastStatus?.auth_attempts_left === 0;
+
+ function LastContactSent(): VNode {
+ return (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {!lastStatus || !deadline || AbsoluteTime.isExpired(deadline) ? (
+ <i18n.Translate>
+ Last TAN code was sent to your address &quot;{lastAddr}
+ &quot; is not valid anymore.
+ </i18n.Translate>
+ ) : (
+ <Attention
+ title={i18n.str`A TAN code was sent to your address "${lastAddr}"`}
+ >
+ <i18n.Translate>
+ You should wait until &quot;
+ <Time format="dd/MM/yyyy HH:mm:ss" timestamp={deadline} />
+ &quot; to send a new one.
+ </i18n.Translate>
+ </Attention>
+ )}
+ </p>
+ );
+ }
- if (!state) {
- return <div>no state</div>;
+ function TryAnotherCode(): VNode {
+ return (
+ <div class="mx-auto mt-4 max-w-xl flex justify-between">
+ <div>
+ <a
+ data-disabled={unableToChangeAddr}
+ href={unableToChangeAddr ? undefined : routeAsk.url({})}
+ class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ >
+ <i18n.Translate>Try with another address</i18n.Translate>
+ </a>
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the contact address anymore.
+ </i18n.Translate>
+ ) : lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the contact address one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the contact address {lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
+ </p>
+ )}
+ </div>
+ <div>
+ <Button
+ type="submit"
+ disabled={!onSendAgain}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSendAgain}
+ >
+ <i18n.Translate>Send new code</i18n.Translate>
+ </Button>
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {lastStatus.pin_transmissions_left < 1 ? (
+ <i18n.Translate>
+ We can&#39;t send you the code anymore.
+ </i18n.Translate>
+ ) : lastStatus.pin_transmissions_left === 1 ? (
+ <i18n.Translate>
+ We can send the code one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ We can send the code {lastStatus.pin_transmissions_left} more
+ times.
+ </i18n.Translate>
+ )}
+ </p>
+ )}
+ </div>
+ </div>
+ );
}
- if (!state.lastTry) {
- return <div>you should do a challenge first</div>;
+ if (cantTryAnymore) {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>Last TAN code can not be used.</i18n.Translate>
+ </h2>
+
+ <LastContactSent />
+ </div>
+
+ <TryAnotherCode />
+ </div>
+ </Fragment>
+ );
}
return (
@@ -159,33 +302,31 @@ export function AnswerChallenge({
Enter the TAN you received to authenticate.
</i18n.Translate>
</h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- {state.lastTry.transmitted ? (
- <i18n.Translate>
- A TAN was sent to your address &quot;{lastEmail}&quot;.
- </i18n.Translate>
- ) : (
- <Attention title={i18n.str`Resend failed`} type="warning">
+ <LastContactSent />
+
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {lastStatus.auth_attempts_left < 1 ? (
<i18n.Translate>
- We recently already sent a TAN to your address &quot;
- {lastEmail}&quot;. A new TAN will not be transmitted again
- before &quot;{state.lastTry.nextSend}&quot;.
+ You can&#39;t check the PIN anymore.
</i18n.Translate>
- </Attention>
- )}
- </p>
- {!lastTryError ? undefined : (
- <p class="mt-2 text-lg leading-8 text-gray-600">
- <i18n.Translate>
- You can try another PIN but just{" "}
- {lastTryError.auth_attempts_left} times more.
- </i18n.Translate>
+ ) : lastStatus.auth_attempts_left === 1 ? (
+ <i18n.Translate>
+ You can check the PIN one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can check the PIN {lastStatus.auth_attempts_left} more
+ times.
+ </i18n.Translate>
+ )}
</p>
)}
</div>
+
<form
method="POST"
- class="mx-auto mt-16 max-w-xl sm:mt-20"
+ class="mx-auto mt-4 max-w-xl"
onSubmit={(e) => {
e.preventDefault();
}}
@@ -219,12 +360,6 @@ export function AnswerChallenge({
/>
</div>
</div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- <i18n.Translate>
- You have {state.lastTry.attemptsLeft} attempts left.
- </i18n.Translate>
- </p>
</div>
<div class="mt-10">
@@ -237,27 +372,9 @@ export function AnswerChallenge({
<i18n.Translate>Check</i18n.Translate>
</Button>
</div>
- <div class="mt-10 flex justify-between">
- <div>
- <a
- href={routeAsk.url({ nonce })}
- class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- >
- <i18n.Translate>Change email</i18n.Translate>
- </a>
- </div>
- <div>
- <Button
- type="submit"
- disabled={!onSendAgain}
- class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- handler={onSendAgain}
- >
- <i18n.Translate>Send code again</i18n.Translate>
- </Button>
- </div>
- </div>
</form>
+
+ <TryAnotherCode />
</div>
</Fragment>
);