diff options
Diffstat (limited to 'packages/challenger-ui/src/pages/AskChallenge.tsx')
-rw-r--r-- | packages/challenger-ui/src/pages/AskChallenge.tsx | 263 |
1 files changed, 263 insertions, 0 deletions
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; +} |