diff options
Diffstat (limited to 'packages')
-rw-r--r-- | packages/challenger-ui/src/Routing.tsx | 1 | ||||
-rw-r--r-- | packages/challenger-ui/src/app.tsx | 25 | ||||
-rw-r--r-- | packages/challenger-ui/src/hooks/challenge.ts | 2 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AnswerChallenge.tsx | 44 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AskChallenge.tsx | 135 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/Setup.tsx | 2 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/challenger.ts | 18 | ||||
-rw-r--r-- | packages/web-util/src/context/activity.ts | 2 | ||||
-rw-r--r-- | packages/web-util/src/context/challenger-api.ts | 14 |
9 files changed, 157 insertions, 86 deletions
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx index f1f4d82d2..6711e4cae 100644 --- a/packages/challenger-ui/src/Routing.tsx +++ b/packages/challenger-ui/src/Routing.tsx @@ -234,6 +234,7 @@ function PublicRounting(): VNode { > <AnswerChallenge nonce={location.values.nonce} + routeAsk={publicPages.ask} onComplete={() => { navigateTo( publicPages.completed.url({ diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx index d85893c07..2b5c5c815 100644 --- a/packages/challenger-ui/src/app.tsx +++ b/packages/challenger-ui/src/app.tsx @@ -15,6 +15,9 @@ */ import { + CacheEvictor, + ChallengerCacheEviction, + assertUnreachable, canonicalizeBaseUrl, getGlobalLogLevel, setGlobalLogLevelFromString, @@ -33,13 +36,28 @@ import { Routing } from "./Routing.js"; // import { BrowserHashNavigationProvider } from "./context/navigation.js"; import { SettingsProvider } from "./context/settings.js"; // import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js"; -import { h } from "preact"; +import { VNode, h } from "preact"; import { strings } from "./i18n/strings.js"; import { ChallengerUiSettings, fetchSettings } from "./settings.js"; import { Frame } from "./pages/Frame.js"; +import { revalidateChallengeSession } from "./hooks/challenge.js"; const WITH_LOCAL_STORAGE_CACHE = false; -export function App() { +const evictBankSwrCache: CacheEvictor<ChallengerCacheEviction> = { + async notifySuccess(op) { + switch (op) { + case ChallengerCacheEviction.CREATE_CHALLENGE: { + await Promise.all([revalidateChallengeSession()]); + return; + } + default: { + assertUnreachable(op); + } + } + }, +}; + +export function App(): VNode { const [settings, setSettings] = useState<ChallengerUiSettings>(); useEffect(() => { fetchSettings(setSettings); @@ -60,6 +78,9 @@ export function App() { <ChallengerApiProvider baseUrl={new URL("/", baseUrl)} frameOnError={Frame} + evictors={{ + challenger: evictBankSwrCache, + }} > <SWRConfig value={{ diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts index 3df10e21e..846242816 100644 --- a/packages/challenger-ui/src/hooks/challenge.ts +++ b/packages/challenger-ui/src/hooks/challenge.ts @@ -35,7 +35,7 @@ export function useChallengeSession( session: SessionId | undefined, ) { const { - lib: { bank: api }, + lib: { challenger: api }, } = useChallengerApiContext(); async function fetcher([n, c, r, s]: [string, string, string, string]) { diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx index 62b7e775d..5e7973b3d 100644 --- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -22,6 +22,7 @@ import { Attention, Button, LocalNotificationBanner, + RouteDefinition, ShowInputErrorLabel, useChallengerApiContext, useLocalNotificationHandler, @@ -36,9 +37,10 @@ export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; type Props = { nonce: string; onComplete: () => void; + routeAsk: RouteDefinition<{ nonce: string }>; }; -export function AnswerChallenge({ nonce, onComplete }: Props): VNode { +export function AnswerChallenge({ nonce, onComplete, routeAsk }: Props): VNode { const { lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); const { state, accepted, completed } = useSessionState(); @@ -54,7 +56,9 @@ export function AnswerChallenge({ nonce, onComplete }: Props): VNode { ? undefined : !state.lastStatus ? undefined - : ((state.lastStatus.last_address as any)["email"] as string); + : !state.lastStatus.last_address + ? undefined + : state.lastStatus.last_address["email"]; const onSendAgain = !state || lastEmail === undefined @@ -62,7 +66,7 @@ export function AnswerChallenge({ nonce, onComplete }: Props): VNode { : withErrorHandler( async () => { if (!lastEmail) return; - return await lib.bank.challenge(nonce, { email: lastEmail }); + return await lib.challenger.challenge(nonce, { email: lastEmail }); }, (ok) => { if ("redirectURL" in ok.body) { @@ -93,11 +97,11 @@ export function AnswerChallenge({ nonce, onComplete }: Props): VNode { ); const onCheck = - lastTryError && lastTryError.exhausted + errors !== undefined || (lastTryError && lastTryError.exhausted) ? undefined : withErrorHandler( async () => { - return lib.bank.solve(nonce, { pin: pin! }); + return lib.challenger.solve(nonce, { pin: pin! }); }, (ok) => { completed(ok.body.redirectURL as URL); @@ -141,7 +145,7 @@ export function AnswerChallenge({ nonce, onComplete }: Props): VNode { <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> - Please enter the TAN you received to authenticate. + Enter the TAN you received to authenticate. </i18n.Translate> </h2> <p class="mt-2 text-lg leading-8 text-gray-600"> @@ -220,15 +224,25 @@ export function AnswerChallenge({ nonce, onComplete }: Props): VNode { <i18n.Translate>Check</i18n.Translate> </Button> </div> - <div class="mt-10"> - <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 again</i18n.Translate> - </Button> + <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> diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx index 76fe6f00a..aaca51db7 100644 --- a/packages/challenger-ui/src/pages/AskChallenge.tsx +++ b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -49,27 +49,26 @@ export function AskChallenge({ const prevEmail = !status || !status.last_address ? undefined : status.last_address["email"]; const regexEmail = - !status || !status.restrictions - ? undefined - : status.restrictions["email"]; + !status || !status.restrictions ? undefined : status.restrictions["email"]; const { lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); - const [email, setEmail] = useState<string | undefined>(prevEmail); + 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` - : regexEmail && regexEmail.regex - ? !new RegExp(regexEmail.regex).test(email) - ? regexEmail.hint - ? regexEmail.hint - : `invalid` - : undefined - : !EMAIL_REGEX.test(email) - ? i18n.str`invalid email` + : !regexTest.test(email) + ? regexHint + : prevEmail !== undefined && email === prevEmail + ? i18n.str`email should be different` : undefined, repeat: !repeat ? i18n.str`required` @@ -82,7 +81,7 @@ export function AskChallenge({ ? undefined : withErrorHandler( async () => { - return lib.bank.challenge(nonce, { email: email! }); + return lib.challenger.challenge(nonce, { email: email! }); }, (ok) => { if ("redirectURL" in ok.body) { @@ -136,11 +135,9 @@ export function AskChallenge({ <Fragment> <Attention title={i18n.str`A code has been sent to ${prevEmail}`}> <i18n.Translate> - You can change the destination or{" "} - <a href={routeSolveChallenge.url({ nonce })}> - <i18n.Translate>complete the challenge here</i18n.Translate> + <a href={routeSolveChallenge.url({ nonce })} class="underline"> + <i18n.Translate>Complete the challenge here.</i18n.Translate> </a> - . </i18n.Translate> </Attention> </Fragment> @@ -171,8 +168,9 @@ export function AskChallenge({ onChange={(e) => { setEmail(e.currentTarget.value); }} + placeholder={prevEmail} readOnly={status.fix_address} - 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" + 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} @@ -181,50 +179,71 @@ export function AskChallenge({ </div> </div> - <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} - /> + {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> - </div> + )} - <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> + {!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> - <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> + {!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> diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx index 400c9b780..f431835aa 100644 --- a/packages/challenger-ui/src/pages/Setup.tsx +++ b/packages/challenger-ui/src/pages/Setup.tsx @@ -36,7 +36,7 @@ export function Setup({ clientId, onCreated }: Props): VNode { const onStart = withErrorHandler( async () => { - return lib.bank.setup(clientId, "secret-token:chal-secret" as AccessToken); + return lib.challenger.setup(clientId, "secret-token:chal-secret" as AccessToken); }, (ok) => { start({ diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts index 8d23ed273..aa530570d 100644 --- a/packages/taler-util/src/http-client/challenger.ts +++ b/packages/taler-util/src/http-client/challenger.ts @@ -1,6 +1,7 @@ import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js"; import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; +import { TalerCoreBankCacheEviction } from "../index.node.js"; import { LibtoolVersion } from "../libtool-version.js"; import { FailCasesByMethod, @@ -22,24 +23,31 @@ import { codecForChallengerTermsOfServiceResponse, codecForInvalidPinResponse, } from "./types.js"; -import { makeBearerTokenAuthHeader } from "./utils.js"; +import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js"; export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> = ResultByMethod<ChallengerHttpClient, prop>; export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> = FailCasesByMethod<ChallengerHttpClient, prop>; +export enum ChallengerCacheEviction { + CREATE_CHALLENGE, +} + /** */ export class ChallengerHttpClient { httpLib: HttpRequestLibrary; + cacheEvictor: CacheEvictor<ChallengerCacheEviction>; public readonly PROTOCOL_VERSION = "1:0:0"; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, - ) { + cacheEvictor?: CacheEvictor<ChallengerCacheEviction>, + ) { this.httpLib = httpClient ?? createPlatformHttpLib(); + this.cacheEvictor = cacheEvictor ?? nullEvictor; } isCompatible(version: string): boolean { @@ -146,8 +154,12 @@ export class ChallengerHttpClient { redirect: "manual", }); switch (resp.status) { - case HttpStatusCode.Ok: + case HttpStatusCode.Ok: { + await this.cacheEvictor.notifySuccess( + ChallengerCacheEviction.CREATE_CHALLENGE, + ); return opSuccessFromHttp(resp, codecForChallengeCreateResponse()); + } case HttpStatusCode.Found: const redirect = resp.headers.get("Location")!; return opFixedSuccess<RedirectResult>({ diff --git a/packages/web-util/src/context/activity.ts b/packages/web-util/src/context/activity.ts index 3cfd83bda..fd366cbe5 100644 --- a/packages/web-util/src/context/activity.ts +++ b/packages/web-util/src/context/activity.ts @@ -67,6 +67,6 @@ export interface BankLib { } export interface ChallengerLib { - bank: ChallengerHttpClient; + challenger: ChallengerHttpClient; } diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts index 960f1db0d..8748f5f69 100644 --- a/packages/web-util/src/context/challenger-api.ts +++ b/packages/web-util/src/context/challenger-api.ts @@ -15,7 +15,9 @@ */ import { + CacheEvictor, ChallengerApi, + ChallengerCacheEviction, ChallengerHttpClient, LibtoolVersion, ObservabilityEvent, @@ -63,7 +65,9 @@ enum VersionHint { NONE, } -type Evictors = Record<string, never>; +type Evictors = { + challenger?: CacheEvictor<ChallengerCacheEviction>; +} type ConfigResult<T> = | undefined @@ -174,10 +178,10 @@ function buildChallengerApiClient( }, }); - const bank = new ChallengerHttpClient(url.href, httpLib); + const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger); async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> { - const resp = await bank.getConfig(); + const resp = await challenger.getConfig(); if (resp.type === "fail") { throw TalerError.fromUncheckedDetail(resp.detail); } @@ -186,9 +190,9 @@ function buildChallengerApiClient( return { getRemoteConfig, - VERSION: bank.PROTOCOL_VERSION, + VERSION: challenger.PROTOCOL_VERSION, lib: { - bank, + challenger, }, onActivity: tracker.subscribe, cancelRequest: httpLib.cancelRequest, |