aboutsummaryrefslogtreecommitdiff
path: root/packages/challenger-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-06-30 23:46:23 -0300
committerSebastian <sebasjm@gmail.com>2024-06-30 23:46:48 -0300
commit6a50e3a0b6d56e6365e2247f2ab1d461b2f1fc05 (patch)
treed912e974be3018e10f7df07b30b8c51bdcc2081d /packages/challenger-ui/src
parente0fa99e21e026e77f3143bf9e62573f9707f2e25 (diff)
downloadwallet-core-6a50e3a0b6d56e6365e2247f2ab1d461b2f1fc05.tar.xz
removed last status
Diffstat (limited to 'packages/challenger-ui/src')
-rw-r--r--packages/challenger-ui/src/Routing.tsx151
-rw-r--r--packages/challenger-ui/src/app.tsx8
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx29
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts23
-rw-r--r--packages/challenger-ui/src/hooks/session.ts98
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx175
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx141
-rw-r--r--packages/challenger-ui/src/pages/CallengeCompleted.tsx22
-rw-r--r--packages/challenger-ui/src/pages/Setup.tsx161
9 files changed, 426 insertions, 382 deletions
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
index f7488cb8d..179263286 100644
--- a/packages/challenger-ui/src/Routing.tsx
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -30,7 +30,6 @@ 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 { Setup } from "./pages/Setup.js";
@@ -45,26 +44,11 @@ export function Routing(): VNode {
}
const publicPages = {
- noinfo: urlPattern<{ nonce: string }>(
- /\/noinfo\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/noinfo/${nonce}`,
- ),
- authorize: urlPattern<{ nonce: string }>(
- /\/authorize\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/authorize/${nonce}`,
- ),
- ask: urlPattern<{ nonce: string }>(
- /\/ask\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/ask/${nonce}`,
- ),
- answer: urlPattern<{ nonce: string }>(
- /\/answer\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/answer/${nonce}`,
- ),
- completed: urlPattern<{ nonce: string }>(
- /\/completed\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/completed/${nonce}`,
- ),
+ noinfo: urlPattern(/\/noinfo/, () => `#/noinfo`),
+ authorize: urlPattern(/\/authorize/, () => `#/authorize`),
+ ask: urlPattern(/\/ask/, () => `#/ask`),
+ answer: urlPattern(/\/answer/, () => `#/answer`),
+ completed: urlPattern(/\/completed/, () => `#/completed`),
setup: urlPattern<{ client: string }>(
/\/setup\/(?<client>[0-9]+)/,
({ client }) => `#/setup/${client}`,
@@ -79,7 +63,7 @@ function safeGetParam(
return ps[n][0];
}
-function safeToURL(s: string | undefined): URL | undefined {
+export function safeToURL(s: string | undefined): URL | undefined {
if (s === undefined) return undefined;
try {
return new URL(s);
@@ -105,58 +89,44 @@ function PublicRounting(): VNode {
return <div>no info</div>;
}
case "setup": {
+ const secret = safeGetParam(location.params, "secret");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_url"),
+ );
+
return (
<Setup
clientId={location.values.client}
- onCreated={(nonce) => {
- navigateTo(publicPages.ask.url({ nonce }));
- //response_type=code
- //client_id=1
- //redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet
- //state=123
+ secret={secret}
+ // "http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet"
+ redirectURL={redirectURL}
+ onCreated={() => {
+ navigateTo(publicPages.ask.url({}));
}}
/>
);
}
case "authorize": {
- const responseType = safeGetParam(location.params, "response_type");
const clientId = safeGetParam(location.params, "client_id");
const redirectURL = safeToURL(
- safeGetParam(location.params, "redirect_uri"),
+ safeGetParam(location.params, "redirect_url"),
);
const state = safeGetParam(location.params, "state");
- // http://localhost:8080/app/#/authorize/ASDASD123?response_type=code&client_id=1&redirect_uri=goog.ecom&state=123
- //
- // http://localhost:8080/app/?response_type=code&client_id=1&redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet&state=123#/authorize/X9668AR2CFC26X55H0M87GJZXGM45VD4SZE05C5SNS5FADPWN220
+ const sessionId: SessionId | undefined =
+ !clientId || !redirectURL || !state
+ ? undefined
+ : {
+ clientId,
+ nonce: location.values.nonce,
+ redirectURL: redirectURL.href,
+ state,
+ };
- if (
- !responseType ||
- !clientId ||
- !redirectURL ||
- !state ||
- responseType !== "code"
- ) {
- return <MissingParams />;
- }
- const sessionId: SessionId = {
- clientId,
- redirectURL: redirectURL.href,
- state,
- };
return (
<CheckChallengeIsUpToDate
- sessionId={sessionId}
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
+ session={sessionId}
onCompleted={() => {
- start(sessionId);
navigateTo(
publicPages.completed.url({
nonce: location.values.nonce,
@@ -164,7 +134,6 @@ function PublicRounting(): VNode {
);
}}
onChangeLeft={() => {
- start(sessionId);
navigateTo(
publicPages.ask.url({
nonce: location.values.nonce,
@@ -172,7 +141,6 @@ function PublicRounting(): VNode {
);
}}
onNoMoreChanges={() => {
- start(sessionId);
navigateTo(
publicPages.ask.url({
nonce: location.values.nonce,
@@ -186,26 +154,9 @@ function PublicRounting(): VNode {
}
case "ask": {
return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- onCompleted={() => {
- navigateTo(
- publicPages.completed.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- >
+ <CheckChallengeIsUpToDate>
<AskChallenge
focus
- nonce={location.values.nonce}
routeSolveChallenge={publicPages.answer}
onSendSuccesful={() => {
navigateTo(
@@ -214,32 +165,22 @@ function PublicRounting(): VNode {
}),
);
}}
+ // onCompleted={() => {
+ // navigateTo(
+ // publicPages.completed.url({
+ // nonce: location.values.nonce,
+ // }),
+ // );
+ // }}
/>
</CheckChallengeIsUpToDate>
);
}
case "answer": {
return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- onCompleted={() => {
- navigateTo(
- publicPages.completed.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- >
+ <CheckChallengeIsUpToDate>
<AnswerChallenge
focus
- nonce={location.values.nonce}
routeAsk={publicPages.ask}
onComplete={() => {
navigateTo(
@@ -248,23 +189,21 @@ function PublicRounting(): VNode {
}),
);
}}
+ // onCompleted={() => {
+ // navigateTo(
+ // publicPages.completed.url({
+ // nonce: location.values.nonce,
+ // }),
+ // );
+ // }}
/>
</CheckChallengeIsUpToDate>
);
}
case "completed": {
return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- >
- <CallengeCompleted nonce={location.values.nonce} />
+ <CheckChallengeIsUpToDate>
+ <CallengeCompleted />
</CheckChallengeIsUpToDate>
);
}
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
index 02ec95107..07b0fe261 100644
--- a/packages/challenger-ui/src/app.tsx
+++ b/packages/challenger-ui/src/app.tsx
@@ -83,9 +83,11 @@ export function App(): VNode {
<ChallengerApiProvider
baseUrl={new URL("/", baseUrl)}
frameOnError={Frame}
- evictors={{
- challenger: evictBankSwrCache,
- }}
+ evictors={
+ {
+ // challenger: evictBankSwrCache,
+ }
+ }
>
<SWRConfig
value={{
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
index 5ac7998d8..1ff7197bf 100644
--- a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -28,42 +28,23 @@ import { useChallengeSession } from "../hooks/challenge.js";
import { SessionId, useSessionState } from "../hooks/session.js";
interface Props {
- nonce: string;
+ session?: SessionId | undefined;
children: ComponentChildren;
- sessionId?: SessionId;
onCompleted?: () => void;
onChangeLeft?: () => void;
onNoMoreChanges?: () => void;
- onNoInfo: () => void;
}
export function CheckChallengeIsUpToDate({
- sessionId: sessionFromParam,
- nonce,
+ session,
children,
onCompleted,
onChangeLeft,
onNoMoreChanges,
- onNoInfo,
}: Props): VNode {
const { state } = useSessionState();
const { i18n } = useTranslationContext();
- const sessionId = sessionFromParam
- ? sessionFromParam
- : !state
- ? undefined
- : {
- clientId: state.clientId,
- redirectURL: state.redirectURL,
- state: state.state,
- };
-
- const result = useChallengeSession(nonce, sessionId);
-
- if (!sessionId) {
- onNoInfo();
- return <Loading />;
- }
+ const result = useChallengeSession(session ?? state);
if (!result) {
return <Loading />;
@@ -111,7 +92,7 @@ export function CheckChallengeIsUpToDate({
}
}
- if (onCompleted && "redirectURL" in result.body) {
+ if (onCompleted && result.body.solved) {
onCompleted();
return <Loading />;
}
@@ -121,7 +102,7 @@ export function CheckChallengeIsUpToDate({
return <Loading />;
}
- if (onChangeLeft && !result.body.changes_left) {
+ if (onChangeLeft && result.body.changes_left) {
onChangeLeft();
return <Loading />;
}
diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
index 224c60b9b..4a641aa26 100644
--- a/packages/challenger-ui/src/hooks/challenge.ts
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -30,27 +30,24 @@ export function revalidateChallengeSession() {
);
}
-export function useChallengeSession(
- nonce: string,
- session: SessionId | undefined,
-) {
+export function useChallengeSession(session: SessionId | undefined) {
const {
lib: { challenger: api },
} = useChallengerApiContext();
- async function fetcher([n, c, r, s]: [string, string, string, string]): Promise<any> {
- return await api.login(n, c, r, s);
+ async function fetcher([s]: [SessionId]) {
+ return await api.login(s.nonce, s.clientId, s.redirectURL, s.state);
}
const { data, error } = useSWR<
ChallengerResultByMethod<"login">,
TalerHttpError
- >(
- !session
- ? undefined
- : [nonce, session.clientId, session.redirectURL, session.state, "login"],
- fetcher,
- {},
- );
+ >(!session ? undefined : [session, "login"], fetcher, {
+ revalidateIfStale: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
if (data) return data;
if (error) return error;
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
index 03cef41bf..f1798885c 100644
--- a/packages/challenger-ui/src/hooks/session.ts
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -16,26 +16,23 @@
import {
AbsoluteTime,
- ChallengerApi,
Codec,
buildCodecForObject,
codecForAbsoluteTime,
codecForAny,
- codecForBoolean,
- codecForChallengeStatus,
- codecForNumber,
codecForString,
codecForStringURL,
codecOptional,
+ ChallengerApi,
} from "@gnu-taler/taler-util";
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
-import { mutate } from "swr";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
export type SessionId = {
+ nonce: string;
clientId: string;
redirectURL: string;
state: string;
@@ -50,29 +47,29 @@ export type LastChallengeResponse = {
};
export type SessionState = SessionId & {
- lastStatus: ChallengerApi.ChallengeStatus | undefined;
completedURL: string | undefined;
lastAddress: Record<string, string> | undefined;
+ lastAddressSavedAt: AbsoluteTime | undefined;
};
export const codecForSessionState = (): Codec<SessionState> =>
buildCodecForObject<SessionState>()
+ .property("nonce", codecForString())
.property("clientId", codecForString())
.property("redirectURL", codecForStringURL())
.property("state", codecForString())
.property("completedURL", codecOptional(codecForStringURL()))
- .property("lastStatus", codecOptional(codecForChallengeStatus()))
.property("lastAddress", codecOptional(codecForAny()))
+ .property("lastAddressSavedAt", codecOptional(codecForAbsoluteTime))
.build("SessionState");
export interface SessionStateHandler {
state: SessionState | undefined;
start(s: SessionId): void;
saveAddress(address: Record<string, string> | undefined): void;
- sent(left: number, nextTime: AbsoluteTime): void;
- failed(left: number): void;
- completed(e: URL): void;
- updateStatus(s: ChallengerApi.ChallengeStatus): void;
+ sent(info: ChallengerApi.ChallengeCreateResponse): void;
+ failed(info: ChallengerApi.InvalidPinResponse): void;
+ completed(info: ChallengerApi.ChallengeRedirect): void;
}
const SESSION_STATE_KEY = buildStorageKey(
@@ -94,47 +91,60 @@ export function useSessionState(): SessionStateHandler {
update({
...info,
completedURL: undefined,
- lastStatus: undefined,
lastAddress: state?.lastAddress,
+ lastAddressSavedAt: state?.lastAddressSavedAt,
+ });
+ // cleanAllCache();
+ },
+ saveAddress(address) {
+ if (!state) throw Error("should have an state");
+ update({
+ ...state,
+ // completedURL: url.href,
+ lastAddress: address,
+ lastAddressSavedAt: AbsoluteTime.now(),
});
- cleanAllCache();
},
- saveAddress(address) {},
- sent(left: number, nextTime: AbsoluteTime) {},
- failed(left: number) {},
- completed(url) {
- if (!state) return;
+ sent(info) {
+ if (!state) throw Error("should have an state");
update({
...state,
- completedURL: url.href,
});
},
- updateStatus(st: ChallengerApi.ChallengeStatus) {
- if (!state) return;
- if (!state.lastStatus) {
- update({
- ...state,
- lastStatus: st,
- });
- return;
- }
- // current status, FIXME: better check to know if the state changed
- const ls = state.lastStatus;
- if (
- ls.changes_left !== st.changes_left ||
- ls.fix_address !== st.fix_address ||
- ls.last_address !== st.last_address
- ) {
- update({
- ...state,
- lastStatus: st,
- });
- return;
- }
+ failed(info) {},
+ completed(info) {
+ if (!state) throw Error("should have an state");
+ update({
+ ...state,
+ completedURL: info.redirect_url,
+ });
},
+ // updateStatus(st: ChallengerApi.ChallengeStatus) {
+ // if (!state) return;
+ // if (!state.lastStatus) {
+ // update({
+ // ...state,
+ // lastStatus: st,
+ // });
+ // return;
+ // }
+ // // current status, FIXME: better check to know if the state changed
+ // const ls = state.lastStatus;
+ // if (
+ // ls.changes_left !== st.changes_left ||
+ // ls.fix_address !== st.fix_address ||
+ // ls.last_address !== st.last_address
+ // ) {
+ // update({
+ // ...state,
+ // lastStatus: st,
+ // });
+ // return;
+ // }
+ // },
};
}
-function cleanAllCache(): void {
- mutate(() => true, undefined, { revalidate: false });
-}
+// function cleanAllCache(): void {
+// mutate(() => true, undefined, { revalidate: false });
+// }
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index ce2589ac5..1576f2cf2 100644
--- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -16,7 +16,9 @@
import {
AbsoluteTime,
ChallengerApi,
+ EmptyObject,
HttpStatusCode,
+ TalerError,
TalerProtocolTimestamp,
assertUnreachable,
} from "@gnu-taler/taler-util";
@@ -32,61 +34,83 @@ import {
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 { useSessionState } from "../hooks/session.js";
+import { useChallengeSession } from "../hooks/challenge.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 {
+function useReloadOnDeadline(deadline: AbsoluteTime): void {
+ const [, set] = useState(false);
+ useEffect(() => {
+ if (AbsoluteTime.isExpired(deadline)) {
+ return;
+ }
+ const diff = AbsoluteTime.difference(AbsoluteTime.now(), deadline);
+ if (diff.d_ms === "forever") return;
+ const p = setTimeout(() => {
+ set(true);
+ }, diff.d_ms);
+ return () => {
+ clearTimeout(p);
+ };
+ }, []);
+}
+
+export function AnswerChallenge({ focus, onComplete, routeAsk }: Props): VNode {
const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const { state, sent, failed, completed } = useSessionState();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [pin, setPin] = useState<string | undefined>();
-
const errors = undefinedIfEmpty({
pin: !pin ? i18n.str`Can't be empty` : undefined,
});
- const lastEmail = !state
+ const result = useChallengeSession(state);
+
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ const lastEmail = !lastStatus?.last_address
? undefined
- : !state.lastStatus
- ? undefined
- : !state.lastStatus.last_address
- ? undefined
- : state.lastStatus.last_address["email"];
+ : lastStatus.last_address["email"];
+ const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1;
const contact = lastEmail ? { email: lastEmail } : undefined;
+ const deadline =
+ lastStatus == undefined
+ ? undefined
+ : AbsoluteTime.fromProtocolTimestamp(lastStatus.retransmission_time);
+
+ useReloadOnDeadline(deadline ?? AbsoluteTime.never());
+
const onSendAgain =
+ !state?.nonce ||
contact === undefined ||
- state?.lastStatus == undefined ||
- state?.lastStatus.changes_left === 0
+ lastStatus == undefined ||
+ lastStatus.auth_attempts_left === 0 ||
+ !deadline ||
+ !AbsoluteTime.isExpired(deadline)
? undefined
: withErrorHandler(
async () => {
- return await lib.challenger.challenge(nonce, contact);
+ 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 {
- sent(
- ok.body.attempts_left,
- AbsoluteTime.fromProtocolTimestamp(ok.body.retransmission_time),
- );
+ sent(ok.body);
}
return undefined;
},
@@ -107,19 +131,20 @@ export function AnswerChallenge({
);
const onCheck =
+ !state?.nonce ||
errors !== undefined ||
- state?.lastStatus == undefined ||
- state?.lastStatus.auth_attempts_left === 0
+ 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 {
- failed(ok.body.pin_transmissions_left);
+ failed(ok.body);
}
onComplete();
},
@@ -144,10 +169,6 @@ export function AnswerChallenge({
},
);
- if (!state) {
- return <div>no state</div>;
- }
-
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
@@ -160,43 +181,50 @@ export function AnswerChallenge({
</i18n.Translate>
</h2>
<p class="mt-2 text-lg leading-8 text-gray-600">
- {state.lastStatus?.last_address ? (
+ {!lastStatus || !deadline || AbsoluteTime.isExpired(deadline) ? (
<i18n.Translate>
- A TAN was sent to your address &quot;{lastEmail}&quot;.
+ Last TAN code was sent to your address &quot;{contact?.email}
+ &quot;.
</i18n.Translate>
) : (
- <Attention title={i18n.str`Resend failed`} type="warning">
+ <Attention title={i18n.str`Unable send the code again`}>
<i18n.Translate>
We recently already sent a TAN to your address &quot;
- {lastEmail}&quot;. A new TAN will not be transmitted again
- before &quot;
+ {contact?.email}&quot;. A new TAN will not be transmitted
+ again before &quot;
<Time
format="dd/MM/yyyy HH:mm:ss"
- timestamp={
- state.lastStatus?.retransmission_time === undefined
- ? undefined
- : AbsoluteTime.fromProtocolTimestamp(
- state.lastStatus?.retransmission_time,
- )
- }
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ lastStatus.retransmission_time,
+ )}
/>
&quot;.
</i18n.Translate>
</Attention>
)}
</p>
- {!state.lastStatus ? undefined : (
+ {lastStatus === undefined ? undefined : (
<p class="mt-2 text-lg leading-8 text-gray-600">
- <i18n.Translate>
- You can try another PIN but just{" "}
- {state.lastStatus.auth_attempts_left} times more.
- </i18n.Translate>
+ {lastStatus.auth_attempts_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t check the PIN anymore.
+ </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-4 max-w-xl sm:mt-20"
+ class="mx-auto mt-4 max-w-xl"
onSubmit={(e) => {
e.preventDefault();
}}
@@ -230,25 +258,6 @@ export function AnswerChallenge({
/>
</div>
</div>
-
- {state.lastStatus === undefined ? undefined : (
- <p class="mt-3 text-sm leading-6 text-gray-400">
- {state.lastStatus.auth_attempts_left < 1 ? (
- <i18n.Translate>
- You can&#39;t check the PIN anymore.
- </i18n.Translate>
- ) : state.lastStatus.auth_attempts_left === 1 ? (
- <i18n.Translate>
- You can check the PIN one last time.
- </i18n.Translate>
- ) : (
- <i18n.Translate>
- You can check the PIN {state.lastStatus.auth_attempts_left}{" "}
- more times.
- </i18n.Translate>
- )}
- </p>
- )}
</div>
<div class="mt-10">
@@ -264,28 +273,26 @@ export function AnswerChallenge({
<div class="mt-10 flex justify-between">
<div>
<a
- data-disabled={
- !state.lastStatus || state.lastStatus.changes_left < 1
- }
- href={routeAsk.url({ nonce })}
+ 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>Change email</i18n.Translate>
</a>
- {state.lastStatus === undefined ? undefined : (
+ {lastStatus === undefined ? undefined : (
<p class="mt-2 text-sm leading-6 text-gray-400">
- {state.lastStatus.changes_left < 1 ? (
+ {lastStatus.changes_left < 1 ? (
<i18n.Translate>
You can&#39;t change the email anymore.
</i18n.Translate>
- ) : state.lastStatus.changes_left === 1 ? (
+ ) : lastStatus.changes_left === 1 ? (
<i18n.Translate>
You can change the email one last time.
</i18n.Translate>
) : (
<i18n.Translate>
- You can change the email {state.lastStatus.changes_left}{" "}
- more times.
+ You can change the email {lastStatus.changes_left} more
+ times.
</i18n.Translate>
)}
</p>
@@ -300,20 +307,20 @@ export function AnswerChallenge({
>
<i18n.Translate>Send code again</i18n.Translate>
</Button>
- {state.lastStatus === undefined ? undefined : (
+ {lastStatus === undefined ? undefined : (
<p class="mt-2 text-sm leading-6 text-gray-400">
- {state.lastStatus.pin_transmissions_left < 1 ? (
+ {lastStatus.pin_transmissions_left < 1 ? (
<i18n.Translate>
We can&#39;t send you the code anymore.
</i18n.Translate>
- ) : state.lastStatus.pin_transmissions_left === 1 ? (
+ ) : lastStatus.pin_transmissions_left === 1 ? (
<i18n.Translate>
We can send the code one last time.
</i18n.Translate>
) : (
<i18n.Translate>
- We can send the code{" "}
- {state.lastStatus.pin_transmissions_left} more times.
+ We can send the code {lastStatus.pin_transmissions_left}{" "}
+ more times.
</i18n.Translate>
)}
</p>
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
index c194f6fd5..63901f1d9 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -13,7 +13,12 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ EmptyObject,
+ HttpStatusCode,
+ TalerError,
+} from "@gnu-taler/taler-util";
import {
Attention,
Button,
@@ -28,21 +33,17 @@ import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useSessionState } from "../hooks/session.js";
import { doAutoFocus } from "./AnswerChallenge.js";
+import { useChallengeSession } from "../hooks/challenge.js";
-type Form = {
- email: string;
-};
export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
type Props = {
- nonce: string;
onSendSuccesful: () => void;
- routeSolveChallenge: RouteDefinition<{ nonce: string }>;
+ routeSolveChallenge: RouteDefinition<EmptyObject>;
focus?: boolean;
};
export function AskChallenge({
- nonce,
onSendSuccesful,
routeSolveChallenge,
focus,
@@ -50,9 +51,6 @@ export function AskChallenge({
const { state, sent, saveAddress, completed } = useSessionState();
const { lib, config } = useChallengerApiContext();
- const status = state?.lastStatus;
- const prevEmail =
- !status || !status.last_address ? undefined : status.last_address["email"];
const regexEmail = !config.restrictions
? undefined
: config.restrictions["email"];
@@ -68,6 +66,17 @@ export function AskChallenge({
const regexHint =
regexEmail && regexEmail.hint ? regexEmail.hint : i18n.str`invalid email`;
+ const result = useChallengeSession(state);
+
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ const prevEmail = !lastStatus?.last_address
+ ? undefined
+ : lastStatus.last_address["email"];
+
const errors = undefinedIfEmpty({
email: !email
? i18n.str`required`
@@ -85,21 +94,20 @@ export function AskChallenge({
const contact = email ? { email } : undefined;
const onSend =
- errors || !contact
+ errors || !contact || !state?.nonce
? undefined
: withErrorHandler(
async () => {
- return lib.challenger.challenge(nonce, contact);
+ return lib.challenger.challenge(state.nonce, contact);
},
(ok) => {
if (ok.body.type === "completed") {
- completed(new URL(ok.body.redirect_url));
+ completed(ok.body);
} else {
- saveAddress(contact);
- sent(
- ok.body.attempts_left,
- AbsoluteTime.fromProtocolTimestamp(ok.body.retransmission_time),
- );
+ if (remember) {
+ saveAddress(contact);
+ }
+ sent(ok.body);
}
onSendSuccesful();
},
@@ -119,7 +127,7 @@ export function AskChallenge({
},
);
- if (!status) {
+ if (!lastStatus) {
return <div>no status loaded</div>;
}
@@ -139,34 +147,26 @@ export function AskChallenge({
</i18n.Translate>
</p>
</div>
- {state.lastStatus?.last_address && (
+
+ {lastStatus?.last_address && (
<Fragment>
<Attention title={i18n.str`A code has been sent to ${prevEmail}`}>
<i18n.Translate>
- <a href={routeSolveChallenge.url({ nonce })} class="underline">
+ <a href={routeSolveChallenge.url({})} class="underline">
<i18n.Translate>Complete the challenge here.</i18n.Translate>
</a>
</i18n.Translate>
</Attention>
</Fragment>
)}
+
<form
method="POST"
- class="mx-auto mt-4 max-w-xl sm:mt-20"
+ class="mx-auto mt-4 max-w-xl "
onSubmit={(e) => {
e.preventDefault();
}}
>
- <div class="py-4">
- <Attention title={i18n.str`A code has been sent to ${prevEmail}`}>
- <i18n.Translate>
- <a href={routeSolveChallenge.url({ nonce })} class="underline">
- <i18n.Translate>Complete the challenge here.</i18n.Translate>
- </a>
- </i18n.Translate>
- </Attention>
- </div>
-
<div class="sm:col-span-2">
<label
for="email"
@@ -187,7 +187,7 @@ export function AskChallenge({
setEmail(e.currentTarget.value);
}}
placeholder={prevEmail}
- readOnly={status.fix_address}
+ readOnly={lastStatus.fix_address}
class="block w-full read-only:bg-slate-200 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
@@ -197,39 +197,7 @@ export function AskChallenge({
</div>
</div>
- <div class="flex items-center justify-between py-2">
- <span class="flex flex-grow flex-col">
- <span
- class="text-sm text-black font-medium leading-6 "
- id="availability-label"
- >
- <i18n.Translate>
- Remember this address for future challenges.
- </i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`remember switch`}
- data-enabled={remember}
- class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
- role="switch"
- aria-checked="false"
- aria-labelledby="availability-label"
- aria-describedby="availability-description"
- onClick={() => {
- setRemember(!remember);
- }}
- >
- <span
- aria-hidden="true"
- data-enabled={remember}
- class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
- ></span>
- </button>
- </div>
-
- {status.fix_address ? undefined : (
+ {lastStatus.fix_address ? undefined : (
<div class="sm:col-span-2">
<label
for="repeat-email"
@@ -257,20 +225,19 @@ export function AskChallenge({
</div>
)}
- {state.lastStatus === undefined ? undefined : (
+ {lastStatus === undefined ? undefined : (
<p class="mt-2 text-sm leading-6 text-gray-400">
- {state.lastStatus.changes_left < 1 ? (
+ {lastStatus.changes_left < 1 ? (
<i18n.Translate>
You can&#39;t change the email anymore.
</i18n.Translate>
- ) : state.lastStatus.changes_left === 1 ? (
+ ) : lastStatus.changes_left === 1 ? (
<i18n.Translate>
You can change the email one last time.
</i18n.Translate>
) : (
<i18n.Translate>
- You can change the email {state.lastStatus.changes_left} more
- times.
+ You can change the email {lastStatus.changes_left} more times.
</i18n.Translate>
)}
</p>
@@ -299,6 +266,38 @@ export function AskChallenge({
</Button>
</div>
)}
+
+ <div class="flex items-center justify-between py-2">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>
+ Remember this address for future challenges.
+ </i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`remember switch`}
+ data-enabled={remember}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setRemember(!remember);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={remember}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
</form>
</div>
</Fragment>
diff --git a/packages/challenger-ui/src/pages/CallengeCompleted.tsx b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
index f8cd7ce60..e897bae5b 100644
--- a/packages/challenger-ui/src/pages/CallengeCompleted.tsx
+++ b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
@@ -14,13 +14,19 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { VNode, h } from "preact";
+import { useChallengeSession } from "../hooks/challenge.js";
+import { useSessionState } from "../hooks/session.js";
+import { TalerError } from "@gnu-taler/taler-util";
-type Props = {
- nonce: string;
+type Props = {};
+export function CallengeCompleted({}: Props): VNode {
+ const { state } = useSessionState();
+ const result = useChallengeSession(state);
+
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ return <div>completed {lastStatus}</div>;
}
-export function CallengeCompleted({nonce}:Props):VNode {
-
- return <div>
- completed {nonce}
- </div>
-} \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx
index f431835aa..c7395f605 100644
--- a/packages/challenger-ui/src/pages/Setup.tsx
+++ b/packages/challenger-ui/src/pages/Setup.tsx
@@ -13,47 +13,84 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AccessToken, HttpStatusCode, encodeCrock, randomBytes } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ createClientSecretAccessToken,
+ createRFC8959AccessTokenEncoded,
+ encodeCrock,
+ randomBytes,
+} from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
+ ShowInputErrorLabel,
useChallengerApiContext,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { safeToURL } from "../Routing.js";
import { useSessionState } from "../hooks/session.js";
+import { doAutoFocus, undefinedIfEmpty } from "./AnswerChallenge.js";
type Props = {
clientId: string;
- onCreated: (nonce:string) => void;
+ secret: string | undefined;
+ redirectURL: URL | undefined;
+ onCreated: () => void;
+ focus?: boolean;
};
-export function Setup({ clientId, onCreated }: Props): VNode {
+export function Setup({
+ clientId,
+ secret,
+ redirectURL,
+ focus,
+ onCreated,
+}: Props): VNode {
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const { lib } = useChallengerApiContext();
const { start } = useSessionState();
+ const [password, setPassword] = useState<string | undefined>(secret);
+ const [url, setUrl] = useState<string | undefined>(redirectURL?.href);
- const onStart = withErrorHandler(
- async () => {
- return lib.challenger.setup(clientId, "secret-token:chal-secret" as AccessToken);
- },
- (ok) => {
- start({
- clientId,
- redirectURL: "http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet",
- state: encodeCrock(randomBytes(32)),
- });
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ url: !url
+ ? i18n.str`required`
+ : !safeToURL(url)
+ ? i18n.str`invalid format`
+ : undefined,
+ });
- onCreated(ok.body.nonce);
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.NotFound:
- return i18n.str`Client doesn't exist.`;
- }
- },
- );
+ const onStart =
+ !!errors || password === undefined || url === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.setup(
+ clientId,
+ createRFC8959AccessTokenEncoded(password),
+ );
+ },
+ (ok) => {
+ start({
+ nonce: ok.body.nonce,
+ clientId,
+ redirectURL: url,
+ state: encodeCrock(randomBytes(32)),
+ });
+
+ onCreated();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.NotFound:
+ return i18n.str`Client doesn't exist.`;
+ }
+ },
+ );
return (
<Fragment>
@@ -67,15 +104,81 @@ export function Setup({ clientId, onCreated }: Props): VNode {
</i18n.Translate>
</h2>
</div>
- <div class="mt-10">
- <Button
- type="submit"
- 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={onStart}
+
+ <form
+ method="POST"
+ class="mx-auto mt-4 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
>
- <i18n.Translate>Start</i18n.Translate>
- </Button>
+ <i18n.Translate>Password</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ ref={focus ? doAutoFocus : undefined}
+ maxLength={512}
+ autocomplete="password"
+ value={password}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value);
+ }}
+ readOnly={secret !== undefined}
+ class="block w-full read-only:bg-slate-200 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?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
</div>
+
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Redirect URL</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="text"
+ name="redirect_url"
+ id="redirect_url"
+ maxLength={512}
+ autocomplete="redirect_url"
+ value={url}
+ onChange={(e) => {
+ setUrl(e.currentTarget.value);
+ }}
+ readOnly={redirectURL !== undefined}
+ class="block w-full read-only:bg-slate-200 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?.url}
+ isDirty={url !== undefined}
+ />
+ </div>
+ </div>
+ </form>
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onStart}
+ 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={onStart}
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </Button>
+ </div>
</div>
</Fragment>
);