summaryrefslogtreecommitdiff
path: root/packages/challenger-ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/challenger-ui')
-rw-r--r--packages/challenger-ui/.gitignore4
-rw-r--r--packages/challenger-ui/Makefile36
-rw-r--r--packages/challenger-ui/README.md4
-rwxr-xr-xpackages/challenger-ui/build.mjs43
-rw-r--r--packages/challenger-ui/copyleft-header.js15
-rwxr-xr-xpackages/challenger-ui/create_must.sh25
-rwxr-xr-xpackages/challenger-ui/dev.mjs56
-rw-r--r--packages/challenger-ui/package.json67
-rw-r--r--packages/challenger-ui/postcss.config.js21
-rw-r--r--packages/challenger-ui/src/Routing.tsx270
-rw-r--r--packages/challenger-ui/src/app.tsx168
-rw-r--r--packages/challenger-ui/src/assets/home.svg3
-rw-r--r--packages/challenger-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/challenger-ui/src/assets/people.svg3
-rw-r--r--packages/challenger-ui/src/attempts-exhausted.html88
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx132
-rw-r--r--packages/challenger-ui/src/context/settings.ts44
-rw-r--r--packages/challenger-ui/src/enter-address-form.html133
-rw-r--r--packages/challenger-ui/src/enter-email-form.html127
-rw-r--r--packages/challenger-ui/src/enter-file-access-form.html102
-rw-r--r--packages/challenger-ui/src/enter-phone-form.html126
-rw-r--r--packages/challenger-ui/src/enter-tan-form.html117
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts58
-rw-r--r--packages/challenger-ui/src/hooks/session.ts143
-rw-r--r--packages/challenger-ui/src/i18n/challenger-ui.pot199
-rw-r--r--packages/challenger-ui/src/i18n/poheader26
-rw-r--r--packages/challenger-ui/src/i18n/strings.ts90
-rw-r--r--packages/challenger-ui/src/index.html41
-rw-r--r--packages/challenger-ui/src/index.tsx27
-rw-r--r--packages/challenger-ui/src/internal-error.html89
-rw-r--r--packages/challenger-ui/src/invalid-pin.html87
-rw-r--r--packages/challenger-ui/src/invalid-request.html88
-rw-r--r--packages/challenger-ui/src/main.js2
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx279
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx263
-rw-r--r--packages/challenger-ui/src/pages/CallengeCompleted.tsx26
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx69
-rw-r--r--packages/challenger-ui/src/pages/MissingParams.tsx22
-rw-r--r--packages/challenger-ui/src/pages/NonceNotFound.tsx42
-rw-r--r--packages/challenger-ui/src/pages/Setup.tsx82
-rw-r--r--packages/challenger-ui/src/scss/main.css3
-rw-r--r--packages/challenger-ui/src/settings.json3
-rw-r--r--packages/challenger-ui/src/settings.ts83
-rw-r--r--packages/challenger-ui/src/validation-unknown.html89
-rw-r--r--packages/challenger-ui/tailwind.config.js28
-rw-r--r--packages/challenger-ui/tsconfig.json46
46 files changed, 3478 insertions, 0 deletions
diff --git a/packages/challenger-ui/.gitignore b/packages/challenger-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/challenger-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/challenger-ui/Makefile b/packages/challenger-ui/Makefile
new file mode 100644
index 000000000..64f9f83d1
--- /dev/null
+++ b/packages/challenger-ui/Makefile
@@ -0,0 +1,36 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run check
+ pnpm run build
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/challenger-ui/README.md b/packages/challenger-ui/README.md
new file mode 100644
index 000000000..ac589ace6
--- /dev/null
+++ b/packages/challenger-ui/README.md
@@ -0,0 +1,4 @@
+# Challenger Backoffice UI
+
+Web-based user interface for the GNU Challenger.
+
diff --git a/packages/challenger-ui/build.mjs b/packages/challenger-ui/build.mjs
new file mode 100755
index 000000000..166647f79
--- /dev/null
+++ b/packages/challenger-ui/build.mjs
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+/*
+ 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 { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/main.js","src/index.tsx"],
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ "src/attempts-exhausted.html",
+ "src/enter-address-form.html",
+ "src/enter-email-form.html",
+ "src/enter-file-access-form.html",
+ "src/enter-phone-form.html",
+ "src/enter-tan-form.html",
+ "src/internal-error.html",
+ "src/invalid-pin.html",
+ "src/invalid-request.html",
+ "src/validation-unknown.html",
+ ]
+ }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/challenger-ui/copyleft-header.js b/packages/challenger-ui/copyleft-header.js
new file mode 100644
index 000000000..7fa276bea
--- /dev/null
+++ b/packages/challenger-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ 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/>
+ */
diff --git a/packages/challenger-ui/create_must.sh b/packages/challenger-ui/create_must.sh
new file mode 100755
index 000000000..a4d78b2cc
--- /dev/null
+++ b/packages/challenger-ui/create_must.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# This file is in the public domain.
+
+# After the compilation succeeded
+# some changes needs to be made
+# in the html/js files to match the
+# what the service expects
+
+cd dist/prod
+
+for file in *.html; do
+
+ # 1. remove the js reference used for dev
+ sed /main.js/d -i $file
+
+ # 2. change the location of css since
+ #challenger backend wants them in the root path
+ sed 's/="main.css"/="..\/main.css"/' -i $file
+
+ # 3. rename the extension to must template
+ mv $file ${file:0:-5}.en.must
+done
+
+#delete unused files
+rm *.js *.map
diff --git a/packages/challenger-ui/dev.mjs b/packages/challenger-ui/dev.mjs
new file mode 100755
index 000000000..595c3e99e
--- /dev/null
+++ b/packages/challenger-ui/dev.mjs
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/*
+ 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 { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
+
+const devEntryPoints = ["src/index.tsx", "src/main.js"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ "src/attempts-exhausted.html",
+ "src/enter-address-form.html",
+ "src/enter-email-form.html",
+ "src/enter-file-access-form.html",
+ "src/enter-phone-form.html",
+ "src/enter-tan-form.html",
+ "src/internal-error.html",
+ "src/invalid-pin.html",
+ "src/invalid-request.html",
+ "src/validation-unknown.html",
+ ]
+ }],
+ },
+ destination: "./dist/dev",
+ public: "/app",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
new file mode 100644
index 000000000..8234e2385
--- /dev/null
+++ b/packages/challenger-ui/package.json
@@ -0,0 +1,67 @@
+{
+ "private": true,
+ "name": "@gnu-taler/challenger-ui",
+ "version": "0.10.7",
+ "author": "sebasjm",
+ "license": "AGPL-3.0-OR-LATER",
+ "description": "UI for GNU Challenger.",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs && ./create_must.sh",
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
+ "pretty": "prettier --write src"
+ },
+ "eslintConfig": {
+ "plugins": [
+ "header"
+ ],
+ "rules": {
+ "header/header": [
+ 2,
+ "copyleft-header.js"
+ ]
+ },
+ "extends": [
+ "prettier"
+ ]
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.17",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "mocha": "9.2.0",
+ "po2json": "^0.4.5",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "challenger-ui"
+ },
+ "dependencies": {
+ "swr": "2.0.3",
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "jed": "1.1.1",
+ "qrcode-generator": "^1.4.4",
+ "preact": "10.11.3"
+ }
+}
diff --git a/packages/challenger-ui/postcss.config.js b/packages/challenger-ui/postcss.config.js
new file mode 100644
index 000000000..c9a60a43c
--- /dev/null
+++ b/packages/challenger-ui/postcss.config.js
@@ -0,0 +1,21 @@
+/*
+ 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/>
+ */
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
new file mode 100644
index 000000000..6166f159a
--- /dev/null
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -0,0 +1,270 @@
+/*
+ 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 {
+ Loading,
+ urlPattern,
+ useCurrentLocation,
+ useNavigationContext,
+} from "@gnu-taler/web-util/browser";
+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 { Setup } from "./pages/Setup.js";
+
+export function Routing(): VNode {
+ // check session and defined if this is
+ // public routing or private
+ return (
+ <Frame>
+ <PublicRounting />
+ </Frame>
+ );
+}
+
+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}`,
+ ),
+ setup: urlPattern<{ client: string }>(
+ /\/setup\/(?<client>[0-9]+)/,
+ ({ client }) => `#/setup/${client}`,
+ ),
+};
+
+function safeGetParam(
+ ps: Record<string, string[]>,
+ n: string,
+): string | undefined {
+ if (!ps[n] || ps[n].length == 0) return undefined;
+ return ps[n][0];
+}
+
+function safeToURL(s: string | undefined): URL | undefined {
+ if (s === undefined) return undefined;
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+function PublicRounting(): VNode {
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { start } = useSessionState();
+
+ if (location === undefined) {
+ return <NonceNotFound />;
+ }
+
+ switch (location.name) {
+ case "noinfo": {
+ return <div>no info</div>;
+ }
+ case "setup": {
+ 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
+ }}
+ />
+ );
+ }
+ case "authorize": {
+ const responseType = safeGetParam(location.params, "response_type");
+ const clientId = safeGetParam(location.params, "client_id");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+ 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
+
+ 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,
+ }),
+ );
+ }}
+ 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 (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <AskChallenge
+ focus
+ nonce={location.values.nonce}
+ routeSolveChallenge={publicPages.answer}
+ onSendSuccesful={() => {
+ navigateTo(
+ publicPages.answer.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,
+ }),
+ );
+ }}
+ >
+ <AnswerChallenge
+ focus
+ nonce={location.values.nonce}
+ routeAsk={publicPages.ask}
+ onComplete={() => {
+ 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>
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
new file mode 100644
index 000000000..2b5c5c815
--- /dev/null
+++ b/packages/challenger-ui/src/app.tsx
@@ -0,0 +1,168 @@
+/*
+ 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 {
+ CacheEvictor,
+ ChallengerCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ChallengerApiProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+// import { BankCoreApiProvider } from "./context/config.js";
+// import { BrowserHashNavigationProvider } from "./context/navigation.js";
+import { SettingsProvider } from "./context/settings.js";
+// import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js";
+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;
+
+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);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ forceLang="en"
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <ChallengerApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={Frame}
+ evictors={{
+ challenger: evictBankSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </ChallengerApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("challenger-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
diff --git a/packages/challenger-ui/src/assets/home.svg b/packages/challenger-ui/src/assets/home.svg
new file mode 100644
index 000000000..35f340162
--- /dev/null
+++ b/packages/challenger-ui/src/assets/home.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/assets/logo-2021.svg b/packages/challenger-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/challenger-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <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" />
+ <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" />
+ <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" />
+ </g>
+ <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" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/assets/people.svg b/packages/challenger-ui/src/assets/people.svg
new file mode 100644
index 000000000..1dc878b81
--- /dev/null
+++ b/packages/challenger-ui/src/assets/people.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
+</svg> \ No newline at end of file
diff --git a/packages/challenger-ui/src/attempts-exhausted.html b/packages/challenger-ui/src/attempts-exhausted.html
new file mode 100644
index 000000000..c2468b98b
--- /dev/null
+++ b/packages/challenger-ui/src/attempts-exhausted.html
@@ -0,0 +1,88 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Attempts exhausted (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ You have tried too many times
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>More attempts are not allowed</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </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>
+
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
new file mode 100644
index 000000000..70e41bf1e
--- /dev/null
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -0,0 +1,132 @@
+/*
+ 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,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useChallengeSession } from "../hooks/challenge.js";
+import { SessionId, useSessionState } from "../hooks/session.js";
+
+interface Props {
+ nonce: string;
+ children: ComponentChildren;
+ sessionId?: SessionId;
+ onCompleted?: () => void;
+ onChangeLeft?: () => void;
+ onNoMoreChanges?: () => void;
+ onNoInfo: () => void;
+}
+export function CheckChallengeIsUpToDate({
+ sessionId: sessionFromParam,
+ nonce,
+ children,
+ onCompleted,
+ onChangeLeft,
+ onNoMoreChanges,
+ onNoInfo,
+}: Props): VNode {
+ const { state, updateStatus } = useSessionState();
+ const { i18n } = useTranslationContext();
+
+ const sessionId = sessionFromParam
+ ? sessionFromParam
+ : !state
+ ? undefined
+ : {
+ clientId: state.clientId,
+ redirectURL: state.redirectURL,
+ state: state.state,
+ };
+
+ const result = useChallengeSession(nonce, sessionId);
+ console.log("asd");
+ if (!sessionId) {
+ onNoInfo();
+ return <Loading />;
+ }
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
+ }
+
+ 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);
+ }
+ }
+
+ 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 />;
+ }
+
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/challenger-ui/src/context/settings.ts b/packages/challenger-ui/src/context/settings.ts
new file mode 100644
index 000000000..679359200
--- /dev/null
+++ b/packages/challenger-ui/src/context/settings.ts
@@ -0,0 +1,44 @@
+/*
+ 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, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { ChallengerUiSettings } from "../settings.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = ChallengerUiSettings;
+
+const initial: ChallengerUiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: ChallengerUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/challenger-ui/src/enter-address-form.html b/packages/challenger-ui/src/enter-address-form.html
new file mode 100644
index 000000000..76b4d2262
--- /dev/null
+++ b/packages/challenger-ui/src/enter-address-form.html
@@ -0,0 +1,133 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <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">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive a letter with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="address" class="block text-sm font-semibold leading-6 text-gray-900">
+ Street address
+ </label>
+ <div class="mt-2.5">
+ <textarea name="address" id="address" rows="3" autocomplete="shipping street-address"
+ class="block w-full rounded-md border-0 py-1.5 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"></textarea>
+
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="city" class="block text-sm font-semibold leading-6 text-gray-900">
+ City
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="city" id="city" maxlength="512" autocomplete="address-level2"
+ 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">
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="postal-code" class="block text-sm font-semibold leading-6 text-gray-900">
+ Postal code
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="postal-code" id="postal-code" maxlength="512" autocomplete="postal-code"
+ 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">
+ </div>
+ </div>
+
+ <div class="sm:col-span-2">
+ <label for="country" class="block text-sm font-semibold leading-6 text-gray-900">
+ Country
+ </label>
+ <div class="mt-2.5">
+ <input type="text" name="country" id="country" maxlength="512" autocomplete="country"
+ 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">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change address another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full 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">
+ Send mail
+ </button>
+ </div>
+ </form>
+ </div>
+ </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>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-email-form.html b/packages/challenger-ui/src/enter-email-form.html
new file mode 100644
index 000000000..3b8720244
--- /dev/null
+++ b/packages/challenger-ui/src/enter-email-form.html
@@ -0,0 +1,127 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <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">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive an email with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <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">
+ Email
+ </label>
+ <div class="mt-2.5">
+ <input type="email" name="email" id="email" maxlength="512" autocomplete="email" value="{{last_address}}"
+ {{#fixed_address}}readonly{{/fixed_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">
+ </div>
+ </div>
+
+ <script>
+ function check() {
+ var email = document.getElementById('email');
+ var emailRepeat = document.getElementById('repeat-email');
+
+ if (email.value != emailRepeat.value) {
+ emailRepeat.setCustomValidity('The two email addresses must match.');
+ } else {
+ // input is valid -- reset the error message
+ emailRepeat.setCustomValidity('');
+ }
+ }
+ </script>
+
+ <div class="sm:col-span-2">
+ <label for="repeat-email" class="block text-sm font-semibold leading-6 text-gray-900">
+ Repeat email
+ </label>
+ <div class="mt-2.5">
+ <input oninput="check(this)" type="email" name="repeat-email" id="repeat-email" 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">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change your email address another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full 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">
+ Send email
+ </button>
+ </div>
+ </form>
+ </div>
+ </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>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-file-access-form.html b/packages/challenger-ui/src/enter-file-access-form.html
new file mode 100644
index 000000000..b79d1dada
--- /dev/null
+++ b/packages/challenger-ui/src/enter-file-access-form.html
@@ -0,0 +1,102 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter local file name</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <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">
+ Enter file name
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ The file will be overwritten with a code which need to be entered in the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Phone number
+ </label>
+ <div class="mt-2.5">
+ <input type="filename" name="filename" id="filename" maxlength="200"
+ 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">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change the filename another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full 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">
+ Write file
+ </button>
+ </div>
+ </form>
+ </div>
+ </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>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-phone-form.html b/packages/challenger-ui/src/enter-phone-form.html
new file mode 100644
index 000000000..ca06fb94e
--- /dev/null
+++ b/packages/challenger-ui/src/enter-phone-form.html
@@ -0,0 +1,126 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2023 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter contact details</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <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">
+ Enter contact details
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ You will receive an SMS with a TAN code that must be provided on the next page.
+ </p>
+ </div>
+ <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Phone number
+ </label>
+ <div class="mt-2.5">
+ <input type="phone" name="phone" id="phone" maxlength="20" autocomplete="tel"
+ 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">
+ </div>
+ </div>
+
+ <script>
+ function check() {
+ var phone = document.getElementById('phone');
+ var phoneRepeat = document.getElementById('repeat-phone');
+
+ if (phone.value != phoneRepeat.value) {
+ phoneRepeat.setCustomValidity('The two phone numbers must match.');
+ } else {
+ // input is valid -- reset the error message
+ phoneRepeat.setCustomValidity('');
+ }
+ }
+ </script>
+
+ <div class="sm:col-span-2">
+ <label for="repeat-phone" class="block text-sm font-semibold leading-6 text-gray-900">
+ Repeat phone
+ </label>
+ <div class="mt-2.5">
+ <input oninput="check(this)" type="number" name="repeat-phone" id="repeat-phone" autocomplete="tel"
+ 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">
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You can change your phone number another {{changes_left}} times.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full 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">
+ Send SMS
+ </button>
+ </div>
+ </form>
+ </div>
+ </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>
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/enter-tan-form.html b/packages/challenger-ui/src/enter-tan-form.html
new file mode 100644
index 000000000..965f8e9d2
--- /dev/null
+++ b/packages/challenger-ui/src/enter-tan-form.html
@@ -0,0 +1,117 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Enter your TAN</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <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">
+ Please enter the TAN you received to authenticate.
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <!-- {{#transmitted}}
+ A TAN was sent to your address &quot;{{address}}&quot;.
+ {{/transmitted}} -->
+ <!-- {{^transmitted}} -->
+ We recently already sent a TAN to your address &quot;{{address}}&quot;.
+ A new TAN will not be transmitted again before {{next_tx_time}}.
+ <!-- {{/transmitted}} -->
+ </p>
+ </div>
+
+
+ <form action="/solve/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
+ <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">
+ TAN code
+ </label>
+ <div class="mt-2.5">
+ <div
+ class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600">
+ <span class="flex select-none items-center pl-3 text-gray-500 sm:text-sm">TAN:</span>
+ <input type="number" name="pin" id="pin" maxlength="64"
+ class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
+ placeholder="12345678">
+ </div>
+
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ You have {{attempts_left}} attempts left.
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <button type="submit"
+ class="block w-full 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">
+ Check
+ </button>
+ </div>
+ </form>
+
+ </div>
+ </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>
+
+</body>
+
+</html>
diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
new file mode 100644
index 000000000..846242816
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -0,0 +1,58 @@
+/*
+ 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 {
+ ChallengerResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
+import { useChallengerApiContext } from "@gnu-taler/web-util/browser";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { SessionId } from "./session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateChallengeSession() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "login",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useChallengeSession(
+ nonce: string,
+ session: SessionId | undefined,
+) {
+ const {
+ lib: { challenger: api },
+ } = useChallengerApiContext();
+
+ async function fetcher([n, c, r, s]: [string, string, string, string]) {
+ return await api.login(n, c, r, s);
+ }
+ const { data, error } = useSWR<
+ ChallengerResultByMethod<"login">,
+ TalerHttpError
+ >(
+ !session
+ ? undefined
+ : [nonce, session.clientId, session.redirectURL, session.state, "login"],
+ fetcher,
+ {},
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
new file mode 100644
index 000000000..ed7ea8986
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -0,0 +1,143 @@
+/*
+ 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,
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForChallengeStatus,
+ codecForNumber,
+ codecForString,
+ codecForStringURL,
+ codecOptional,
+} 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 = {
+ clientId: string;
+ redirectURL: string;
+ state: string;
+};
+
+export type LastChallengeResponse = {
+ attemptsLeft: number;
+ nextSend: string;
+ transmitted: boolean;
+};
+
+export type SessionState = SessionId & {
+ lastTry: LastChallengeResponse | undefined;
+ lastStatus: ChallengerApi.ChallengeStatus | undefined;
+ completedURL: string | undefined;
+};
+export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
+ buildCodecForObject<LastChallengeResponse>()
+ .property("attemptsLeft", codecForNumber())
+ .property("nextSend", codecForString())
+ .property("transmitted", codecForBoolean())
+ .build("LastChallengeResponse");
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForObject<SessionState>()
+ .property("clientId", codecForString())
+ .property("redirectURL", codecForStringURL())
+ .property("completedURL", codecOptional(codecForStringURL()))
+ .property("state", codecForString())
+ .property("lastStatus", codecOptional(codecForChallengeStatus()))
+ .property("lastTry", codecOptional(codecForLastChallengeResponse()))
+ .build("SessionState");
+
+export interface SessionStateHandler {
+ state: SessionState | undefined;
+ start(s: SessionId): void;
+ accepted(l: LastChallengeResponse): void;
+ completed(e: URL): void;
+ updateStatus(s: ChallengerApi.ChallengeStatus): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "challenger-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(SESSION_STATE_KEY);
+
+ return {
+ state,
+ start(info) {
+ update({
+ ...info,
+ lastTry: undefined,
+ completedURL: undefined,
+ lastStatus: undefined,
+ });
+ cleanAllCache();
+ },
+ accepted(lastTry) {
+ if (!state) return;
+ update({
+ ...state,
+ lastTry,
+ });
+ },
+ completed(url) {
+ if (!state) return;
+ update({
+ ...state,
+ completedURL: url.href,
+ });
+ },
+ updateStatus(st: ChallengerApi.ChallengeStatus) {
+ if (!state) return;
+ if (!state.lastStatus) {
+ update({
+ ...state,
+ lastStatus: st,
+ });
+ return;
+ }
+ // current status
+ 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 });
+}
diff --git a/packages/challenger-ui/src/i18n/challenger-ui.pot b/packages/challenger-ui/src/i18n/challenger-ui.pot
new file mode 100644
index 000000000..5d2497acf
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/challenger-ui.pot
@@ -0,0 +1,199 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/pages/AnswerChallenge.tsx:55
+#, c-format
+msgid "Can't be empty"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:81
+#, c-format
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:108
+#, c-format
+msgid "Invalid request"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:111
+#, c-format
+msgid "Invalid pin"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:142
+#, c-format
+msgid "Please enter the TAN you received to authenticate."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:147
+#, c-format
+msgid "A TAN was sent to your address &quot;%1$s&quot;."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:151
+#, c-format
+msgid ""
+"We recently already sent a TAN to your address &quot; %1$s&quot;. A new TAN will "
+"not be transmitted again before %2$s."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:161
+#, c-format
+msgid "You can try another PIN but just %1$s times more."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:181
+#, c-format
+msgid "TAN code"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:204
+#, c-format
+msgid "You have %1$s attempts left."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:217
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:227
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:76
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:84
+#, c-format
+msgid "invalid email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:86
+#, c-format
+msgid "emails don't match"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:130
+#, c-format
+msgid "Enter contact details"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:133
+#, c-format
+msgid ""
+"You will receive an email with a TAN code that must be provided on the next "
+"page."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:152
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:180
+#, c-format
+msgid "Repeat email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:198
+#, c-format
+msgid "You can change your email address another %1$s times."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:211
+#, c-format
+msgid "Send email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:237
+#, c-format
+msgid "Bad request"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:238
+#, c-format
+msgid "Could not start the challenge, check configuration."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:246
+#, c-format
+msgid "Not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:247
+#, c-format
+msgid "Nonce not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:253
+#, c-format
+msgid "Not acceptable"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:254
+#, c-format
+msgid "Server has wrong template configuration"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:262
+#, c-format
+msgid "Internal error"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:263
+#, c-format
+msgid "Check logs"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:33
+#, c-format
+msgid "The URL is wrong"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:36
+#, c-format
+msgid "Maybe the validation check expired."
+msgstr ""
+
+#: src/pages/Setup.tsx:53
+#, c-format
+msgid "Client doesn't exist."
+msgstr ""
+
+#: src/pages/Setup.tsx:65
+#, c-format
+msgid "Setup new challenge with client ID: &quot;%1$s&quot;"
+msgstr ""
+
+#: src/pages/Setup.tsx:76
+#, c-format
+msgid "Start"
+msgstr ""
+
diff --git a/packages/challenger-ui/src/i18n/poheader b/packages/challenger-ui/src/i18n/poheader
new file mode 100644
index 000000000..f2c9d10dd
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/poheader
@@ -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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Challenger UI\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/challenger-ui/src/i18n/strings.ts b/packages/challenger-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..ea13fed2e
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/strings.ts
@@ -0,0 +1,90 @@
+/*
+ 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/>
+ */
+export interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "en",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/challenger-ui/src/index.html b/packages/challenger-ui/src/index.html
new file mode 100644
index 000000000..18f472045
--- /dev/null
+++ b/packages/challenger-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Challenger</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/challenger-ui/src/index.tsx b/packages/challenger-ui/src/index.tsx
new file mode 100644
index 000000000..f559288a3
--- /dev/null
+++ b/packages/challenger-ui/src/index.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
+
+const app = document.getElementById("app");
+
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
+}
diff --git a/packages/challenger-ui/src/internal-error.html b/packages/challenger-ui/src/internal-error.html
new file mode 100644
index 000000000..521a2b69e
--- /dev/null
+++ b/packages/challenger-ui/src/internal-error.html
@@ -0,0 +1,89 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Internal server error (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Internal error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}} ({{detail}})</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </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>
+
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-pin.html b/packages/challenger-ui/src/invalid-pin.html
new file mode 100644
index 000000000..1229b8095
--- /dev/null
+++ b/packages/challenger-ui/src/invalid-pin.html
@@ -0,0 +1,87 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Invalid solution (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Invalid PIN
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </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>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-request.html b/packages/challenger-ui/src/invalid-request.html
new file mode 100644
index 000000000..89e6b125c
--- /dev/null
+++ b/packages/challenger-ui/src/invalid-request.html
@@ -0,0 +1,88 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Invalid request (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Request error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </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>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/main.js b/packages/challenger-ui/src/main.js
new file mode 100644
index 000000000..3272bd0bb
--- /dev/null
+++ b/packages/challenger-ui/src/main.js
@@ -0,0 +1,2 @@
+// intentionally empty
+import "./scss/main.css"; \ No newline at end of file
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 &quot;{lastEmail}&quot;.
+ </i18n.Translate>
+ ) : (
+ <Attention title={i18n.str`Resend failed`} type="warning">
+ <i18n.Translate>
+ We recently already sent a TAN to your address &quot;
+ {lastEmail}&quot;. A new TAN will not be transmitted again
+ before &quot;{state.lastTry.nextSend}&quot;.
+ </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: &quot;{clientId}&quot;
+ </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>
+ );
+}
diff --git a/packages/challenger-ui/src/scss/main.css b/packages/challenger-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/challenger-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/challenger-ui/src/settings.json b/packages/challenger-ui/src/settings.json
new file mode 100644
index 000000000..b3d0476aa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "backendBaseURL": "http://challenger.taler.test:1180/"
+}
diff --git a/packages/challenger-ui/src/settings.ts b/packages/challenger-ui/src/settings.ts
new file mode 100644
index 000000000..61d2117fa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.ts
@@ -0,0 +1,83 @@
+/*
+ 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 {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+
+export interface ChallengerUiSettings {
+ // Where challenger backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: ChallengerUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+};
+
+const codecForChallengerUISettings = (): Codec<ChallengerUiSettings> =>
+ buildCodecForObject<ChallengerUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .build("codecForChallengerUISettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: ChallengerUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForChallengerUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/challenger-ui/src/validation-unknown.html b/packages/challenger-ui/src/validation-unknown.html
new file mode 100644
index 000000000..56c8d156c
--- /dev/null
+++ b/packages/challenger-ui/src/validation-unknown.html
@@ -0,0 +1,89 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <link rel="stylesheet" href="main.css" />
+ <script type="module" src="main.js"></script>
+ <title>Validation process unknown (#{{ec}})</title>
+</head>
+
+<body class="min-h-full flex flex-col">
+ <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=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%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">
+
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
+ <div class="rounded-md bg-red-50 p-4 shadow-xl">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
+ class="w-8 h-8 text-red-400">
+ <path fill-rule="evenodd"
+ d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ </svg>
+ </div>
+ <div class="ml-3">
+ <h3 class="text-sm font-medium text-red-800">
+ Validation error
+ </h3>
+ <div class="mt-2 text-sm text-red-700">
+ <p>{{hint}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </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>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/challenger-ui/tailwind.config.js b/packages/challenger-ui/tailwind.config.js
new file mode 100644
index 000000000..d384690e2
--- /dev/null
+++ b/packages/challenger-ui/tailwind.config.js
@@ -0,0 +1,28 @@
+/*
+ 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/>
+ */
+export default {
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/challenger-ui/tsconfig.json b/packages/challenger-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/challenger-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "allowJs": true /* Allow javascript files to be compiled. */,
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*"]
+}