diff options
Diffstat (limited to 'packages/challenger-ui/src')
-rw-r--r-- | packages/challenger-ui/src/Routing.tsx | 90 | ||||
-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.ts | 30 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AnswerChallenge.tsx | 87 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AskChallenge.tsx | 138 |
5 files changed, 207 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 "{state.email}". </i18n.Translate> ) : ( - <i18n.Translate> - We recently already sent a TAN to your address " - {state.email}". 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 " + {state.email}". A new TAN will not be transmitted again + before "{state.lastTry.nextSend}". + </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, |