diff options
Diffstat (limited to 'packages/challenger-ui/src/pages')
-rw-r--r-- | packages/challenger-ui/src/pages/AnswerChallenge.tsx | 279 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AskChallenge.tsx | 263 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/CallengeCompleted.tsx | 26 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/Frame.tsx | 69 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/MissingParams.tsx | 22 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/NonceNotFound.tsx | 42 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/Setup.tsx | 82 |
7 files changed, 783 insertions, 0 deletions
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx new file mode 100644 index 000000000..73a79c51f --- /dev/null +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -0,0 +1,279 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { + ChallengerApi, + HttpStatusCode, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Attention, + Button, + LocalNotificationBanner, + RouteDefinition, + ShowInputErrorLabel, + useChallengerApiContext, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +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 }>; +}; + +export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props): VNode { + const { lib } = useChallengerApiContext(); + const { i18n } = useTranslationContext(); + const { state, accepted, 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 + ? undefined + : !state.lastStatus + ? undefined + : !state.lastStatus.last_address + ? undefined + : state.lastStatus.last_address["email"]; + + const onSendAgain = + !state || lastEmail === undefined + ? undefined + : withErrorHandler( + async () => { + if (!lastEmail) return; + return await lib.challenger.challenge(nonce, { email: lastEmail }); + }, + (ok) => { + if ("redirectURL" in ok.body) { + completed(ok.body.redirectURL); + } else { + accepted({ + attemptsLeft: ok.body.attempts_left, + nextSend: ok.body.next_tx_time, + transmitted: ok.body.transmitted, + }); + } + return undefined; + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str``; + case HttpStatusCode.NotFound: + return i18n.str``; + case HttpStatusCode.NotAcceptable: + return i18n.str``; + case HttpStatusCode.TooManyRequests: + return i18n.str``; + case HttpStatusCode.InternalServerError: + return i18n.str``; + } + }, + ); + + const onCheck = + errors !== undefined || (lastTryError && lastTryError.exhausted) + ? undefined + : withErrorHandler( + async () => { + return lib.challenger.solve(nonce, { pin: pin! }); + }, + (ok) => { + completed(ok.body.redirectURL as URL); + onComplete(); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`Invalid request`; + case HttpStatusCode.Forbidden: { + setLastTryError(fail.body); + return i18n.str`Invalid pin`; + } + case HttpStatusCode.NotFound: + return i18n.str``; + case HttpStatusCode.NotAcceptable: + return i18n.str``; + case HttpStatusCode.TooManyRequests: + return i18n.str``; + case HttpStatusCode.InternalServerError: + return i18n.str``; + default: + assertUnreachable(fail); + } + }, + ); + + if (!state) { + return <div>no state</div>; + } + + if (!state.lastTry) { + return <div>you should do a challenge first</div>; + } + + 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> + 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 "{lastEmail}". + </i18n.Translate> + ) : ( + <Attention title={i18n.str`Resend failed`} type="warning"> + <i18n.Translate> + We recently already sent a TAN to your address " + {lastEmail}". A new TAN will not be transmitted again + before "{state.lastTry.nextSend}". + </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> + </p> + )} + </div> + <form + method="POST" + class="mx-auto mt-16 max-w-xl sm:mt-20" + onSubmit={(e) => { + e.preventDefault(); + }} + > + <div class="grid grid-cols-1 gap-x-8 gap-y-6"> + <div class="sm:col-span-2"> + <label + for="pin" + class="block text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>TAN code</i18n.Translate> + </label> + <div class="mt-2.5"> + <input + autoFocus + ref={focus ? doAutoFocus : undefined} + type="number" + name="pin" + id="pin" + maxLength={64} + value={pin} + onChange={(e) => { + setPin(e.currentTarget.value); + }} + placeholder="12345678" + 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?.pin} + isDirty={pin !== undefined} + /> + </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"> + <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" + disabled={!onCheck} + handler={onCheck} + > + <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> + </div> + </Fragment> + ); +} + +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocus(element: HTMLElement | null): void { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + element.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + }, 100); + } +} + +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; +} diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx new file mode 100644 index 000000000..30b50d707 --- /dev/null +++ b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -0,0 +1,263 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + Attention, + Button, + LocalNotificationBanner, + RouteDefinition, + ShowInputErrorLabel, + useChallengerApiContext, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { useSessionState } from "../hooks/session.js"; +import { doAutoFocus } from "./AnswerChallenge.js"; + +type Form = { + email: string; +}; +export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; + +type Props = { + nonce: string; + onSendSuccesful: () => void; + routeSolveChallenge: RouteDefinition<{ nonce: string }>; + focus?: boolean; +}; + +export function AskChallenge({ + nonce, + onSendSuccesful, + routeSolveChallenge, + focus, +}: Props): VNode { + const { state, accepted, completed } = useSessionState(); + const status = state?.lastStatus; + const prevEmail = + !status || !status.last_address ? undefined : status.last_address["email"]; + const regexEmail = + !status || !status.restrictions ? undefined : status.restrictions["email"]; + + const { lib } = useChallengerApiContext(); + const { i18n } = useTranslationContext(); + const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [email, setEmail] = useState<string | undefined>(); + const [repeat, setRepeat] = useState<string | undefined>(); + + const regexTest = + regexEmail && regexEmail.regex ? new RegExp(regexEmail.regex) : EMAIL_REGEX; + const regexHint = + regexEmail && regexEmail.hint ? regexEmail.hint : i18n.str`invalid email`; + + const errors = undefinedIfEmpty({ + email: !email + ? i18n.str`required` + : !regexTest.test(email) + ? regexHint + : prevEmail !== undefined && email === prevEmail + ? i18n.str`email should be different` + : undefined, + repeat: !repeat + ? i18n.str`required` + : email !== repeat + ? i18n.str`emails doesn't match` + : undefined, + }); + + const onSend = errors + ? undefined + : withErrorHandler( + async () => { + return lib.challenger.challenge(nonce, { email: email! }); + }, + (ok) => { + if ("redirectURL" in ok.body) { + completed(ok.body.redirectURL); + } else { + accepted({ + attemptsLeft: ok.body.attempts_left, + nextSend: ok.body.next_tx_time, + transmitted: ok.body.transmitted, + }); + } + onSendSuccesful(); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str``; + case HttpStatusCode.NotFound: + return i18n.str``; + case HttpStatusCode.NotAcceptable: + return i18n.str``; + case HttpStatusCode.TooManyRequests: + return i18n.str``; + case HttpStatusCode.InternalServerError: + return i18n.str``; + } + }, + ); + + if (!status) { + return <div>no status loaded</div>; + } + + 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>Enter contact details</i18n.Translate> + </h2> + <p class="mt-2 text-lg leading-8 text-gray-600"> + <i18n.Translate> + You will receive an email with a TAN code that must be provided on + the next page. + </i18n.Translate> + </p> + </div> + {state.lastTry && ( + <Fragment> + <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> + </Fragment> + )} + <form + method="POST" + class="mx-auto mt-16 max-w-xl sm:mt-20" + onSubmit={(e) => { + e.preventDefault(); + }} + > + <div class="grid grid-cols-1 gap-x-8 gap-y-6"> + <div class="sm:col-span-2"> + <label + for="email" + class="block text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Email</i18n.Translate> + </label> + <div class="mt-2.5"> + <input + type="email" + name="email" + id="email" + ref={focus ? doAutoFocus : undefined} + maxLength={512} + autocomplete="email" + value={email} + onChange={(e) => { + setEmail(e.currentTarget.value); + }} + placeholder={prevEmail} + readOnly={status.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 + message={errors?.email} + isDirty={email !== undefined} + /> + </div> + </div> + + {status.fix_address ? undefined : ( + <div class="sm:col-span-2"> + <label + for="repeat-email" + class="block text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Repeat email</i18n.Translate> + </label> + <div class="mt-2.5"> + <input + type="email" + name="repeat-email" + id="repeat-email" + value={repeat} + onChange={(e) => { + setRepeat(e.currentTarget.value); + }} + 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> + )} + + {!status.changes_left ? ( + <p class="mt-3 text-sm leading-6 text-gray-400"> + <i18n.Translate>No more changes left</i18n.Translate> + </p> + ) : ( + <p class="mt-3 text-sm leading-6 text-gray-400"> + <i18n.Translate> + You can change your email address another{" "} + {status.changes_left} times. + </i18n.Translate> + </p> + )} + </div> + + {!prevEmail ? ( + <div class="mt-10"> + <Button + type="submit" + disabled={!onSend} + 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={onSend} + > + <i18n.Translate>Send email</i18n.Translate> + </Button> + </div> + ) : ( + <div class="mt-10"> + <Button + type="submit" + disabled={!onSend} + 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={onSend} + > + <i18n.Translate>Change email</i18n.Translate> + </Button> + </div> + )} + </form> + </div> + </Fragment> + ); +} + +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; +} diff --git a/packages/challenger-ui/src/pages/CallengeCompleted.tsx b/packages/challenger-ui/src/pages/CallengeCompleted.tsx new file mode 100644 index 000000000..f8cd7ce60 --- /dev/null +++ b/packages/challenger-ui/src/pages/CallengeCompleted.tsx @@ -0,0 +1,26 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { VNode, h } from "preact"; + +type Props = { + nonce: string; +} +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/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx new file mode 100644 index 000000000..612eced0b --- /dev/null +++ b/packages/challenger-ui/src/pages/Frame.tsx @@ -0,0 +1,69 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { ComponentChildren, Fragment, h, VNode } from "preact"; + +export function Frame({ children }: { children: ComponentChildren }): VNode { + return ( + <Fragment> + <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400"> + <div class="flex flex-row h-16 items-center "> + <div class="flex px-2 justify-start"> + <div class="flex-shrink-0 bg-white rounded-lg"> + <a href="#"> + <img + class="h-8 w-auto" + src='data:image/svg+xml,<?xml version="1.0" encoding="UTF-8" standalone="no"?>%0A<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">%0A <g fill="%230042b3" fill-rule="evenodd" stroke-width=".3">%0A <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />%0A <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />%0A <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />%0A </g>%0A <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />%0A</svg>' + alt="GNU Taler" + style="height: 1.5rem; margin: 0.5rem;" + /> + </a> + </div> + <span class="flex items-center text-white text-lg font-bold ml-4"> + Challenger + </span> + </div> + <div class="block flex-1 ml-6 "></div> + <div class="flex justify-end"></div> + </div> + </header> + + <main class="flex-1">{children}</main> + + <footer class="bottom-4 mb-4"> + <div class="mt-8 mx-8 md:order-1 md:mt-0"> + <div> + <p class="text-xs leading-5 text-gray-400"> + Learn more about{" "} + <a + target="_blank" + rel="noreferrer noopener" + class="font-semibold text-gray-500 hover:text-gray-400" + href="https://taler.net" + > + GNU Taler + </a> + </p> + </div> + <div style="flex-grow: 1;"></div> + <p class="text-xs leading-5 text-gray-400"> + Copyright © 2014—2023 Taler Systems SA.{" "} + </p> + </div> + </footer> + </Fragment> + ); +} diff --git a/packages/challenger-ui/src/pages/MissingParams.tsx b/packages/challenger-ui/src/pages/MissingParams.tsx new file mode 100644 index 000000000..5eb1e434e --- /dev/null +++ b/packages/challenger-ui/src/pages/MissingParams.tsx @@ -0,0 +1,22 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { VNode, h } from "preact"; + +export function MissingParams() :VNode { + return <div> + missing params: {window.location.href} + </div> +}
\ No newline at end of file diff --git a/packages/challenger-ui/src/pages/NonceNotFound.tsx b/packages/challenger-ui/src/pages/NonceNotFound.tsx new file mode 100644 index 000000000..16b3d90ef --- /dev/null +++ b/packages/challenger-ui/src/pages/NonceNotFound.tsx @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; + +type Form = { + email: string; +}; + +export function NonceNotFound(): VNode { + const { i18n } = useTranslationContext(); + + return ( + <Fragment> + <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>The URL is wrong</i18n.Translate> + </h2> + <p class="mt-2 text-lg leading-8 text-gray-600"> + <i18n.Translate>Maybe the validation check expired.</i18n.Translate> + </p> + </div> + </div> + </Fragment> + ); +} diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx new file mode 100644 index 000000000..f431835aa --- /dev/null +++ b/packages/challenger-ui/src/pages/Setup.tsx @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + 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 { + Button, + LocalNotificationBanner, + useChallengerApiContext, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useSessionState } from "../hooks/session.js"; + +type Props = { + clientId: string; + onCreated: (nonce:string) => void; +}; +export function Setup({ clientId, onCreated }: Props): VNode { + const { i18n } = useTranslationContext(); + const [notification, withErrorHandler] = useLocalNotificationHandler(); + const { lib } = useChallengerApiContext(); + const { start } = useSessionState(); + + 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)), + }); + + onCreated(ok.body.nonce); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.NotFound: + return i18n.str`Client doesn't exist.`; + } + }, + ); + + 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> + Setup new challenge with client ID: "{clientId}" + </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} + > + <i18n.Translate>Start</i18n.Translate> + </Button> + </div> + </div> + </Fragment> + ); +} |