summaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/home
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
committerFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
commit3e060b80428943c6562250a6ff77eff10a0259b7 (patch)
treed08472bc5ca28621c62ac45b229207d8215a9ea7 /packages/demobank-ui/src/pages/home
parentfb52ced35ac872349b0e1062532313662552ff6c (diff)
downloadwallet-core-3e060b80428943c6562250a6ff77eff10a0259b7.tar.gz
wallet-core-3e060b80428943c6562250a6ff77eff10a0259b7.tar.bz2
wallet-core-3e060b80428943c6562250a6ff77eff10a0259b7.zip
repo: integrate packages from former merchant-backoffice.git
Diffstat (limited to 'packages/demobank-ui/src/pages/home')
-rw-r--r--packages/demobank-ui/src/pages/home/index.tsx2018
1 files changed, 2018 insertions, 0 deletions
diff --git a/packages/demobank-ui/src/pages/home/index.tsx b/packages/demobank-ui/src/pages/home/index.tsx
new file mode 100644
index 000000000..bf9764b78
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/index.tsx
@@ -0,0 +1,2018 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import useSWR, { SWRConfig as _SWRConfig, useSWRConfig } from 'swr';
+import { h, Fragment, VNode, createContext } from 'preact';
+import { useRef, useState, useEffect, StateUpdater, useContext } from 'preact/hooks';
+import { Buffer } from 'buffer';
+import { useTranslator, Translate } from '../../i18n';
+import { QR } from '../../components/QR';
+import { useNotNullLocalStorage, useLocalStorage } from '../../hooks';
+import '../../scss/main.scss';
+import talerLogo from '../../assets/logo-white.svg';
+import { LangSelectorLikePy as LangSelector } from '../../components/menu/LangSelector';
+
+// FIXME: Fix usages of SWRConfig, doing this isn't the best practice (but hey, it works for now)
+const SWRConfig = _SWRConfig as any;
+
+const UI_ALLOW_REGISTRATIONS = ('__LIBEUFIN_UI_ALLOW_REGISTRATIONS__') ?? 1;
+const UI_IS_DEMO = ('__LIBEUFIN_UI_IS_DEMO__') ?? 0;
+const UI_BANK_NAME = ('__LIBEUFIN_UI_BANK_NAME__') ?? 'Taler Bank';
+
+/**
+ * FIXME:
+ *
+ * - INPUT elements have their 'required' attribute ignored.
+ *
+ * - the page needs a "home" button that either redirects to
+ * the profile page (when the user is logged in), or to
+ * the very initial home page.
+ *
+ * - histories 'pages' are grouped in UL elements that cause
+ * the rendering to visually separate each UL. History elements
+ * should instead line up without any separation caused by
+ * a implementation detail.
+ *
+ * - Many strings need to be i18n-wrapped.
+ */
+
+/***********
+ * Globals *
+ **********/
+
+/************
+ * Contexts *
+ ***********/
+const CurrencyContext = createContext<any>(null);
+const PageContext = createContext<any>(null);
+
+/**********************************************
+ * Type definitions for states and API calls. *
+ *********************************************/
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+interface BackendStateType {
+ url: string;
+ username: string;
+ password: string;
+}
+
+/**
+ * Request body of POST /transactions.
+ *
+ * If the amount appears twice: both as a Payto parameter and
+ * in the JSON dedicate field, the one on the Payto URI takes
+ * precedence.
+ */
+interface TransactionRequestType {
+ paytoUri: string;
+ amount?: string; // with currency.
+}
+
+/**
+ * Request body of /register.
+ */
+interface CredentialsRequestType {
+ username: string;
+ password: string;
+}
+
+/**
+ * Request body of /register.
+ */
+interface LoginRequestType {
+ username: string;
+ password: string;
+}
+
+interface WireTransferRequestType {
+ iban: string;
+ subject: string;
+ amount: string;
+}
+
+interface Amount {
+ value: string;
+ currency: string;
+}
+
+/**
+ * Track page state.
+ */
+interface PageStateType {
+ isLoggedIn: boolean;
+ isRawPayto: boolean;
+ tryRegister: boolean;
+ showPublicHistories: boolean;
+ hasError: boolean;
+ hasInfo: boolean;
+ withdrawalInProgress: boolean;
+ error?: string;
+ info?: string;
+ talerWithdrawUri?: string;
+ /**
+ * Not strictly a presentational value, could
+ * be moved in a future "withdrawal state" object.
+ */
+ withdrawalId?: string;
+}
+
+/**
+ * Bank account specific information.
+ */
+interface AccountStateType {
+ balance: string;
+ /* FIXME: Need history here. */
+}
+
+/************
+ * Helpers. *
+ ***********/
+
+function maybeDemoContent(content: VNode) {
+ if (UI_IS_DEMO) return content;
+}
+
+async function fetcher(url: string) {
+ return fetch(url).then((r) => (r.json()));
+}
+
+function genCaptchaNumbers(): string {
+ return `${Math.floor(Math.random() * 10)} + ${Math.floor(Math.random() * 10)}`;
+}
+/**
+ * Bring the state to show the public accounts page.
+ */
+function goPublicAccounts(pageStateSetter: StateUpdater<PageStateType>) {
+ return () => pageStateSetter((prevState) => ({ ...prevState, showPublicHistories: true }))
+}
+
+/**
+ * Validate (the number part of) an amount. If needed,
+ * replace comma with a dot. Returns 'false' whenever
+ * the input is invalid, the valid amount otherwise.
+ */
+function validateAmount(maybeAmount: string): any {
+ const amountRegex = '^[0-9]+(\.[0-9]+)?$';
+ if (!maybeAmount) {
+ console.log(`Entered amount (${maybeAmount}) mismatched <input> pattern.`);
+ return;
+ }
+ if (typeof maybeAmount !== 'undefined' || maybeAmount !== '') {
+ console.log(`Maybe valid amount: ${maybeAmount}`);
+ // tolerating comma instead of point.
+ const re = RegExp(amountRegex)
+ if (!re.test(maybeAmount)) {
+ console.log(`Not using invalid amount '${maybeAmount}'.`);
+ return false;
+ }
+ }
+ return maybeAmount;
+}
+
+/**
+ * Extract IBAN from a Payto URI.
+ */
+function getIbanFromPayto(url: string): string {
+ const pathSplit = new URL(url).pathname.split('/');
+ let lastIndex = pathSplit.length - 1;
+ // Happens if the path ends with "/".
+ if (pathSplit[lastIndex] === '') lastIndex--;
+ const iban = pathSplit[lastIndex];
+ return iban;
+}
+
+/**
+ * Extract value and currency from a $currency:x.y amount.
+ */
+function parseAmount(val: string): Amount {
+ const format = /^[A-Z]+:[0-9]+(\.[0-9]+)?$/;
+ if (!format.test(val))
+ throw Error(`Backend gave invalid amount: ${val}.`)
+ const amountSplit = val.split(':');
+ return { value: amountSplit[1], currency: amountSplit[0] }
+}
+
+/**
+ * Get username from the backend state, and throw
+ * exception if not found.
+ */
+function getUsername(backendState: BackendStateTypeOpt): string {
+ if (typeof backendState === 'undefined')
+ throw Error('Username can\'t be found in a undefined backend state.')
+
+ return backendState.username;
+}
+
+/**
+ * Helps extracting the credentials from the state
+ * and wraps the actual call to 'fetch'. Should be
+ * enclosed in a try-catch block by the caller.
+ */
+async function postToBackend(
+ uri: string,
+ backendState: BackendStateTypeOpt,
+ body: string
+): Promise<any> {
+ if (typeof backendState === 'undefined')
+ throw Error('Credentials can\'t be found in a undefined backend state.')
+
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(uri, backendState.url)
+ return await fetch(url.href, {
+ method: 'POST',
+ headers,
+ body,
+ }
+ );
+}
+
+function useTransactionPageNumber(): [number, StateUpdater<number>] {
+ const ret = useNotNullLocalStorage('transaction-page', '0');
+ const retObj = JSON.parse(ret[0]);
+ const retSetter: StateUpdater<number> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter];
+}
+
+/**
+ * Craft headers with Authorization and Content-Type.
+ */
+function prepareHeaders(username: string, password: string) {
+ const headers = new Headers();
+ headers.append(
+ 'Authorization',
+ `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
+ );
+ headers.append(
+ 'Content-Type',
+ 'application/json'
+ )
+ return headers;
+}
+
+// Window can be mocked this way:
+// https://gist.github.com/theKashey/07090691c0a4680ed773375d8dbeebc1#file-webpack-conf-js
+// That allows the app to be pointed to a arbitrary
+// euFin backend when launched via "pnpm dev".
+const getRootPath = () => {
+ const maybeRootPath = typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : '/';
+ if (!maybeRootPath.endsWith('/')) return `${maybeRootPath}/`;
+ return maybeRootPath;
+};
+
+/*******************
+ * State managers. *
+ ******************/
+
+/**
+ * Stores in the state a object containing a 'username'
+ * and 'password' field, in order to avoid losing the
+ * handle of the data entered by the user in <input> fields.
+ */
+function useShowPublicAccount(
+ state?: string
+): [string | undefined, StateUpdater<string | undefined>] {
+
+ const ret = useLocalStorage('show-public-account', JSON.stringify(state));
+ const retObj: string | undefined = ret[0] ? JSON.parse(ret[0]) : ret[0];
+ const retSetter: StateUpdater<string | undefined> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Stores the raw Payto value entered by the user in the state.
+ */
+type RawPaytoInputType = string;
+type RawPaytoInputTypeOpt = RawPaytoInputType | undefined;
+function useRawPaytoInputType(
+ state?: RawPaytoInputType
+): [RawPaytoInputTypeOpt, StateUpdater<RawPaytoInputTypeOpt>] {
+
+ const ret = useLocalStorage('raw-payto-input-state', state);
+ const retObj: RawPaytoInputTypeOpt = ret[0];
+ const retSetter: StateUpdater<RawPaytoInputTypeOpt> = function (val) {
+ const newVal = val instanceof Function ? val(retObj) : val
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Stores in the state a object representing a wire transfer,
+ * in order to avoid losing the handle of the data entered by
+ * the user in <input> fields. FIXME: name not matching the
+ * purpose, as this is not a HTTP request body but rather the
+ * state of the <input>-elements.
+ */
+type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
+function useWireTransferRequestType(
+ state?: WireTransferRequestType
+): [WireTransferRequestTypeOpt, StateUpdater<WireTransferRequestTypeOpt>] {
+
+ const ret = useLocalStorage('wire-transfer-request-state', JSON.stringify(state));
+ const retObj: WireTransferRequestTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0];
+ const retSetter: StateUpdater<WireTransferRequestTypeOpt> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Stores in the state a object containing a 'username'
+ * and 'password' field, in order to avoid losing the
+ * handle of the data entered by the user in <input> fields.
+ */
+type CredentialsRequestTypeOpt = CredentialsRequestType | undefined;
+function useCredentialsRequestType(
+ state?: CredentialsRequestType
+): [CredentialsRequestTypeOpt, StateUpdater<CredentialsRequestTypeOpt>] {
+
+ const ret = useLocalStorage('credentials-request-state', JSON.stringify(state));
+ const retObj: CredentialsRequestTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0];
+ const retSetter: StateUpdater<CredentialsRequestTypeOpt> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+type BackendStateTypeOpt = BackendStateType | undefined;
+function useBackendState(
+ state?: BackendStateType
+): [BackendStateTypeOpt, StateUpdater<BackendStateTypeOpt>] {
+
+ const ret = useLocalStorage('backend-state', JSON.stringify(state));
+ const retObj: BackendStateTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0];
+ const retSetter: StateUpdater<BackendStateTypeOpt> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Keep mere business information, like account balance or
+ * transactions history.
+ */
+type AccountStateTypeOpt = AccountStateType | undefined;
+function useAccountState(
+ state?: AccountStateType
+): [AccountStateTypeOpt, StateUpdater<AccountStateTypeOpt>] {
+
+ const ret = useLocalStorage('account-state', JSON.stringify(state));
+ const retObj: AccountStateTypeOpt = ret[0] ? JSON.parse(ret[0]) : ret[0];
+ const retSetter: StateUpdater<AccountStateTypeOpt> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter]
+}
+
+/**
+ * Wrapper providing defaults.
+ */
+function usePageState(
+ state: PageStateType = {
+ isLoggedIn: false,
+ isRawPayto: false,
+ tryRegister: false,
+ showPublicHistories: false,
+ hasError: false,
+ hasInfo: false,
+ withdrawalInProgress: false,
+ }
+): [PageStateType, StateUpdater<PageStateType>] {
+ const ret = useNotNullLocalStorage('page-state', JSON.stringify(state));
+ const retObj: PageStateType = JSON.parse(ret[0]);
+ console.log('Current page state', retObj);
+ const retSetter: StateUpdater<PageStateType> = function (val) {
+ const newVal = val instanceof Function ? JSON.stringify(val(retObj)) : JSON.stringify(val)
+ console.log('Setting new page state', newVal)
+ ret[1](newVal)
+ }
+ return [retObj, retSetter];
+}
+
+/**
+ * Request preparators.
+ *
+ * These functions aim at sanitizing the input received
+ * from users - for example via a HTML form - and create
+ * a HTTP request object out of that.
+ */
+
+/******************
+ * HTTP wrappers. *
+ *****************/
+
+/**
+ * A 'wrapper' is typically a function that prepares one
+ * particular API call and updates the state accordingly. */
+
+/**
+ * Abort a withdrawal operation via the Access API's /abort.
+ */
+async function abortWithdrawalCall(
+ backendState: BackendStateTypeOpt,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+ if (typeof backendState === 'undefined') {
+ console.log('No credentials found.');
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true, error: 'No credentials found.' }))
+ return;
+ }
+ if (typeof withdrawalId === 'undefined') {
+ console.log('No withdrawal ID found.');
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true, error: 'No withdrawal ID found.' }))
+ return;
+ }
+ let res:any;
+ try {
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ /**
+ * NOTE: tests show that when a same object is being
+ * POSTed, caching might prevent same requests from being
+ * made. Hence, trying to POST twice the same amount might
+ * get silently ignored. Needs more observation!
+ *
+ * headers.append("cache-control", "no-store");
+ * headers.append("cache-control", "no-cache");
+ * headers.append("pragma", "no-cache");
+ * */
+
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(
+ `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
+ backendState.url
+ )
+ res = await fetch(url.href, { method: 'POST', headers })
+ } catch (error) {
+ console.log('Could not abort the withdrawal', error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not abort the withdrawal: ${error}`
+ }))
+ return;
+ }
+ if (!res.ok) {
+ console.log(`Withdrawal abort gave response error (${res.status})`, res.statusText);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Withdrawal abortion gave response error (${res.status})`
+ }))
+ return;
+ }
+ console.log('Withdrawal operation aborted!');
+ pageStateSetter((prevState) => {
+ const { ...rest } = prevState;
+ return {
+ ...rest,
+ info: 'Withdrawal aborted!'
+ }
+ })
+
+}
+
+/**
+ * This function confirms a withdrawal operation AFTER
+ * the wallet has given the exchange's payment details
+ * to the bank (via the Integration API). Such details
+ * can be given by scanning a QR code or by passing the
+ * raw taler://withdraw-URI to the CLI wallet.
+ *
+ * This function will set the confirmation status in the
+ * 'page state' and let the related components refresh.
+ */
+async function confirmWithdrawalCall(
+ backendState: BackendStateTypeOpt,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+
+ if (typeof backendState === 'undefined') {
+ console.log('No credentials found.');
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true, error: 'No credentials found.' }))
+ return;
+ }
+ if (typeof withdrawalId === 'undefined') {
+ console.log('No withdrawal ID found.');
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true, error: 'No withdrawal ID found.' }))
+ return;
+ }
+ let res: Response;
+ try {
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ /**
+ * NOTE: tests show that when a same object is being
+ * POSTed, caching might prevent same requests from being
+ * made. Hence, trying to POST twice the same amount might
+ * get silently ignored.
+ *
+ * headers.append("cache-control", "no-store");
+ * headers.append("cache-control", "no-cache");
+ * headers.append("pragma", "no-cache");
+ * */
+
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(
+ `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
+ backendState.url
+ )
+ res = await fetch(url.href, {
+ method: 'POST',
+ headers
+ })
+ } catch (error) {
+ console.log('Could not POST withdrawal confirmation to the bank', error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not confirm the withdrawal: ${error}`
+ }))
+ return;
+ }
+ if (res ? !res.ok : true) { // assume not ok if res is null
+ console.log(`Withdrawal confirmation gave response error (${res.status})`, res.statusText);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Withdrawal confirmation gave response error (${res.status})`
+ }))
+ return;
+ }
+ console.log('Withdrawal operation confirmed!');
+ pageStateSetter((prevState) => {
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+ info: 'Withdrawal confirmed!'
+ }
+ })
+
+}
+
+/**
+ * This function creates a new transaction. It reads a Payto
+ * address entered by the user and POSTs it to the bank. No
+ * sanity-check of the input happens before the POST as this is
+ * already conducted by the backend.
+ */
+async function createTransactionCall(
+ req: TransactionRequestType,
+ backendState: BackendStateTypeOpt,
+ pageStateSetter: StateUpdater<PageStateType>,
+ /**
+ * Optional since the raw payto form doesn't have
+ * a stateful management of the input data yet.
+ */
+ cleanUpForm: () => void
+) {
+ let res:any;
+ try {
+ res = await postToBackend(
+ `access-api/accounts/${getUsername(backendState)}/transactions`,
+ backendState,
+ JSON.stringify(req)
+ )
+ }
+ catch (error) {
+ console.log('Could not POST transaction request to the bank', error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not create the wire transfer: ${error}`
+ }))
+ return;
+ }
+ // POST happened, status not sure yet.
+ if (!res.ok) {
+ const responseText = JSON.stringify(await res.json());
+ console.log(`Transfer creation gave response error: ${responseText} (${res.status})`);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Transfer creation gave response error: ${responseText} (${res.status})`
+ }))
+ return;
+ }
+ // status is 200 OK here, tell the user.
+ console.log('Wire transfer created!');
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasInfo: true,
+ info: 'Wire transfer created!'
+ }))
+
+ // Only at this point the input data can
+ // be discarded.
+ cleanUpForm();
+}
+
+/**
+ * This function creates a withdrawal operation via the Access API.
+ *
+ * After having successfully created the withdrawal operation, the
+ * user should receive a QR code of the "taler://withdraw/" type and
+ * supposed to scan it with their phone.
+ *
+ * TODO: (1) after the scan, the page should refresh itself and inform
+ * the user about the operation's outcome. (2) use POST helper. */
+async function createWithdrawalCall(
+ amount: string,
+ backendState: BackendStateTypeOpt,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+ if (typeof backendState === 'undefined') {
+ console.log('Page has a problem: no credentials found in the state.');
+ pageStateSetter((prevState) => ({ ...prevState, hasError: true, error: 'No credentials given.' }))
+ return;
+ }
+
+ let res:any;
+ try {
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+
+ // Let bank generate withdraw URI:
+ const url = new URL(
+ `access-api/accounts/${backendState.username}/withdrawals`,
+ backendState.url
+ )
+ res = await fetch(url.href, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ amount }),
+ }
+ );
+ } catch (error) {
+ console.log('Could not POST withdrawal request to the bank', error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Could not create withdrawal operation: ${error}`
+ }))
+ return;
+ }
+ if (!res.ok) {
+ const responseText = await res.text();
+ console.log(`Withdrawal creation gave response error: ${responseText} (${res.status})`);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: `Withdrawal creation gave response error: ${responseText} (${res.status})`
+ }))
+ return;
+ }
+
+ console.log('Withdrawal operation created!');
+ const resp = await res.json();
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ withdrawalInProgress: true,
+ talerWithdrawUri: resp.taler_withdraw_uri,
+ withdrawalId: resp.withdrawal_id
+ }))
+}
+
+async function loginCall(
+ req: CredentialsRequestType,
+ /**
+ * FIXME: figure out if the two following
+ * functions can be retrieved from the state.
+ */
+ backendStateSetter: StateUpdater<BackendStateTypeOpt>,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+
+ /**
+ * Optimistically setting the state as 'logged in', and
+ * let the Account component request the balance to check
+ * whether the credentials are valid. */
+ pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
+ let baseUrl = getRootPath();
+ if (!baseUrl.endsWith('/'))
+ baseUrl += '/';
+
+ backendStateSetter((prevState) => ({
+ ...prevState,
+ url: baseUrl,
+ username: req.username,
+ password: req.password,
+ }));
+}
+
+
+/**
+ * This function requests /register.
+ *
+ * This function is responsible to change two states:
+ * the backend's (to store the login credentials) and
+ * the page's (to indicate a successful login or a problem).
+ */
+async function registrationCall(
+ req: CredentialsRequestType,
+ /**
+ * FIXME: figure out if the two following
+ * functions can be retrieved somewhat from
+ * the state.
+ */
+ backendStateSetter: StateUpdater<BackendStateTypeOpt>,
+ pageStateSetter: StateUpdater<PageStateType>
+) {
+
+ let baseUrl = getRootPath();
+ /**
+ * If the base URL doesn't end with slash and the path
+ * is not empty, then the concatenation made by URL()
+ * drops the last path element.
+ */
+ if (!baseUrl.endsWith('/'))
+ baseUrl += '/'
+
+ const headers = new Headers();
+ headers.append(
+ 'Content-Type',
+ 'application/json'
+ )
+ const url = new URL('access-api/testing/register', baseUrl)
+ let res:any;
+ try {
+ res = await fetch(url.href, {
+ method: 'POST',
+ body: JSON.stringify(req),
+ headers
+ });
+ } catch (error) {
+ console.log(`Could not POST new registration to the bank (${url.href})`, error);
+ pageStateSetter((prevState) => ({
+ ...prevState, hasError: true, error: 'Registration failed, please report.'
+ }));
+ return;
+ }
+ if (!res.ok) {
+ const errorRaw = await res.text();
+ console.log(`New registration gave response error (${res.status})`, errorRaw);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ error: errorRaw
+ }));
+ } else {
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ isLoggedIn: true,
+ tryRegister: false
+ }));
+ backendStateSetter((prevState) => ({
+ ...prevState,
+ url: baseUrl,
+ username: req.username,
+ password: req.password,
+ }));
+ }
+}
+
+/**************************
+ * Functional components. *
+ *************************/
+
+function Currency(): VNode {
+ const { data, error } = useSWR(`${getRootPath()}integration-api/config`, fetcher);
+ if (typeof error !== 'undefined')
+ return <b>error: currency could not be retrieved</b>;
+
+ if (typeof data === 'undefined') return <Fragment>"..."</Fragment>;
+ console.log('found bank config', data);
+ return data.currency;
+}
+
+function ErrorBanner(Props: any): VNode | null {
+ const [pageState, pageStateSetter] = Props.pageState;
+ const i18n = useTranslator();
+ if (!pageState.hasError) return null;
+
+ const rval = (
+ <p class="informational informational-fail">{pageState.error}
+ </p>);
+ delete pageState.error;
+ pageState.hasError = false;
+ return rval;
+}
+
+function StatusBanner(Props: any): VNode | null {
+ const [pageState, pageStateSetter] = Props.pageState;
+ const i18n = useTranslator();
+ if (!pageState.hasInfo) return null;
+
+ const rval = (
+ <p class="informational">{pageState.error}
+ </p>);
+ delete pageState.info_msg;
+ pageState.hasInfo = false;
+ return rval;
+}
+
+function BankFrame(Props: any): VNode {
+ const i18n = useTranslator();
+ const [pageState, pageStateSetter] = useContext(PageContext);
+ console.log('BankFrame state', pageState);
+ const logOut = (
+ <div class="logout">
+ <a
+ href="#"
+ class="pure-button logout-button"
+ onClick={() => {
+ pageStateSetter((prevState: PageStateType) => {
+ const {
+ talerWithdrawUri,
+ withdrawalId, ...rest } = prevState;
+ return {
+ ...rest,
+ isLoggedIn: false,
+ withdrawalInProgress: false,
+ hasInfo: false,
+ hasError: false,
+ isRawPayto: false
+ };
+ });
+ }}>{i18n`Logout`}</a></div>);
+
+ // Prepare demo sites links.
+ const DEMO_SITES = [
+ ['Landing', '__DEMO_SITE_LANDING_URL__'],
+ ['Bank', '__DEMO_SITE_BANK_URL__'],
+ ['Essay Shop', '__DEMO_SITE_BLOG_URL__'],
+ ['Donations', '__DEMO_SITE_DONATIONS_URL__'],
+ ['Survey', '__DEMO_SITE_SURVEY_URL__'],
+ ];
+ const demo_sites = [];
+ for (const i in DEMO_SITES)
+ demo_sites.push(<a href={DEMO_SITES[i][1]}>{DEMO_SITES[i][0]}</a>)
+
+ return (
+ <Fragment>
+ <header class="demobar" style="display: flex; flex-direction: row; justify-content: space-between;">
+ <a href="#main" class="skip">{i18n`Skip to main content`}</a>
+ <div style="max-width: 50em; margin-left: 2em;">
+ <h1>
+ <span class="it">
+ <a href="/">{
+ UI_BANK_NAME
+ }
+ </a>
+ </span>
+ </h1>{
+ maybeDemoContent(<p><Translate>
+ This part of the demo shows how a bank that supports
+ Taler directly would work. In addition to using your own
+ bank account, you can also see the transaction history of
+ some <a href="#" onClick={goPublicAccounts(pageStateSetter)}>Public Accounts</a>.
+ </Translate></p>
+ )
+ }
+ </div>
+ <a href="https://taler.net/">
+ <img
+ src={talerLogo}
+ alt="{i18n`Taler logo`}"
+ height="100"
+ width="224"
+ style="margin: 2em 2em" />
+ </a>
+ </header>
+ <div style="display:flex; flex-direction: column;" class="navcontainer">
+ <nav class="demolist">
+ {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)}
+ <div class="right">
+ <LangSelector />
+ </div>
+ </nav>
+ </div>
+ <section id="main" class="content">
+ <ErrorBanner pageState={[pageState, pageStateSetter]} />
+ <StatusBanner pageState={[pageState, pageStateSetter]} />
+ {pageState.isLoggedIn ? logOut : null}
+ {Props.children}
+ </section>
+ <section id="footer" class="footer">
+ <div class="footer">
+ <hr />
+ <div>
+ <p>You can learn more about GNU Taler on our <a href="https://taler.net">main website</a>.</p>
+ </div>
+ <div style="flex-grow:1" />
+ <p>Copyright &copy; 2014&mdash;2022 Taler Systems SA</p>
+ </div>
+ </section>
+ </Fragment>);
+}
+
+
+function PaytoWireTransfer(Props: any): VNode {
+ const currency = useContext(CurrencyContext);
+ const [pageState, pageStateSetter] = useContext(PageContext); // NOTE: used for go-back button?
+ const [submitData, submitDataSetter] = useWireTransferRequestType();
+ const [rawPaytoInput, rawPaytoInputSetter] = useRawPaytoInputType();
+ const i18n = useTranslator();
+ const { focus, backendState } = Props
+ const amountRegex = '^[0-9]+(\.[0-9]+)?$';
+ const ibanRegex = '^[A-Z][A-Z][0-9]+$';
+ const receiverInput = '';
+ const subjectInput = '';
+ let transactionData: TransactionRequestType;
+ const ref = useRef<HTMLInputElement>(null)
+ useEffect(() => {
+ if (focus) ref.current?.focus();
+ }, [focus, pageState.isRawPayto]);
+
+ if (!pageState.isRawPayto)
+ return (
+ <div>
+ <div class="pure-form"
+ name="wire-transfer-form">
+ <p>
+ <label for="iban">{i18n`Receiver IBAN:`}</label>&nbsp;
+ <input
+ ref={ref}
+ type="text"
+ id="iban"
+ name="iban"
+ value={submitData?.iban}
+ placeholder="CC0123456789"
+ required
+ pattern={ibanRegex}
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ iban: e.currentTarget.value,
+ }))
+ }} /><br /><br />
+ <label for="subject">{i18n`Transfer subject:`}</label>&nbsp;
+ <input
+ type="text"
+ name="subject"
+ id="subject"
+ placeholder="subject"
+ value={submitData?.subject}
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ subject: e.currentTarget.value,
+ }))
+ }} /><br /><br />
+ <label for="amount">{i18n`Amount:`}</label>&nbsp;
+ <input
+ type="number"
+ name="amount"
+ id="amount"
+ placeholder="amount"
+ required
+ value={submitData?.amount}
+ pattern={amountRegex}
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ amount: e.currentTarget.value.replace(',', '.'),
+ }))
+ }} />
+ &nbsp;
+ <input
+ type="text"
+ readonly
+ class="currency-indicator"
+ size={currency.length}
+ maxLength={currency.length}
+ tabIndex={-1} value={currency} />
+ </p>
+ <p>
+ <input
+ type="submit"
+ class="pure-button pure-button-primary"
+ value="Send"
+ onClick={async () => {
+ if (
+ typeof submitData === 'undefined'
+ || (typeof submitData.iban === 'undefined'
+ || submitData.iban === '')
+ || (typeof submitData.subject === 'undefined'
+ || submitData.subject === '')
+ || (typeof submitData.amount === 'undefined'
+ || submitData.amount === '')
+ ) {
+ console.log('Not all the fields were given.');
+ pageStateSetter((prevState: PageStateType) =>
+ ({ ...prevState, hasError: true, error: i18n`Field(s) missing.` }))
+ return;
+ }
+ transactionData = {
+ paytoUri: `payto://iban/${submitData.iban}?message=${encodeURIComponent(submitData.subject)}`,
+ amount: `${currency}:${submitData.amount}`
+ };
+ return await createTransactionCall(
+ transactionData,
+ backendState,
+ pageStateSetter,
+ () => submitDataSetter(p => ({
+ amount: '',
+ iban: '',
+ subject: ''
+ }))
+ );
+ }} />
+ </p>
+ </div>
+ <p><a
+ href="#"
+ onClick={() => {
+ console.log('switch to raw payto form');
+ pageStateSetter((prevState: any) => ({ ...prevState, isRawPayto: true }));
+ }}>{i18n`Want to try the raw payto://-format?`}
+ </a></p>
+ </div>
+ );
+
+ return (
+ <div>
+ <p>
+ {i18n`Transfer money to account identified by payto:// URI:`}
+ </p>
+ <div class="pure-form"
+ name="payto-form">
+ <p>
+ <label for="address">{i18n`payto URI:`}</label>&nbsp;
+ <input
+ name="address"
+ type="text"
+ size={90}
+ ref={ref}
+ id="address"
+ value={rawPaytoInput}
+ required
+ placeholder={i18n`payto address`}
+ pattern={`payto://iban/[A-Z][A-Z][0-9]+\?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(\.[0-9]+)?`}
+ onInput={(e): void => {
+ rawPaytoInputSetter(e.currentTarget.value)
+ }} />
+ <br />
+ <div class="hint">
+ Hint:
+ <code>
+ payto://iban/[receiver-iban]?message=[subject]&amount=[{currency}:X.Y]
+ </code>
+ </div>
+ </p>
+ <p>
+ <input class="pure-button pure-button-primary"
+ type="submit"
+ value={i18n`Send`}
+ onClick={async () => {
+ // empty string evaluates to false.
+ if (!rawPaytoInput) {
+ console.log('Didn\'t get any raw Payto string!');
+ return;
+ }
+ transactionData = { paytoUri: rawPaytoInput };
+ if (typeof transactionData.paytoUri === 'undefined' ||
+ transactionData.paytoUri.length === 0) return;
+
+ return await createTransactionCall(
+ transactionData,
+ backendState,
+ pageStateSetter,
+ () => rawPaytoInputSetter(p => '')
+ );
+ }} />
+ </p>
+ <p><a
+ href="#"
+ onClick={() => {
+ console.log('switch to wire-transfer-form');
+ pageStateSetter((prevState: any) => ({ ...prevState, isRawPayto: false }));
+ }}>{i18n`Use wire-transfer form?`}
+ </a></p>
+ </div>
+ </div>);
+}
+
+/**
+ * Additional authentication required to complete the operation.
+ * Not providing a back button, only abort.
+ */
+function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
+ const [pageState, pageStateSetter] = useContext(PageContext);
+ const { backendState } = Props;
+ const i18n = useTranslator();
+ const captchaNumbers = {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10)
+ }
+ let captchaAnswer = '';
+
+ return (<Fragment>
+ <h1 class="nav">{i18n`Confirm Withdrawal`}</h1>
+ <article>
+ <div class="challenge-div">
+ <form class="challenge-form">
+ <div class="pure-form"
+ id="captcha"
+ name="capcha-form">
+ <h2>{i18n`Authorize withdrawal by solving challenge`}</h2>
+ <p>
+ <label for="answer">{i18n`What is`}&nbsp;<em>{captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}</em>?&nbsp;</label>&nbsp;
+ <input
+ name="answer"
+ id="answer"
+ type="text"
+ required
+ onInput={(e): void => {
+ captchaAnswer = e.currentTarget.value;
+ }} />
+ </p>
+ <p>
+ <button
+ class="pure-button pure-button-primary btn-confirm"
+ onClick={() => {
+ if (captchaAnswer == (captchaNumbers.a + captchaNumbers.b).toString()) {
+ confirmWithdrawalCall(
+ backendState,
+ pageState.withdrawalId,
+ pageStateSetter)
+ return;
+ }
+ pageStateSetter((prevState: PageStateType) =>
+ ({ ...prevState, hasError: true, error: i18n`Answer is wrong.` }))
+ }}>
+ {i18n`Confirm`}
+ </button>
+ &nbsp;
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={() =>
+ abortWithdrawalCall(
+ backendState,
+ pageState.withdrawalId,
+ pageStateSetter
+ )}>
+ {i18n`Cancel`}
+ </button>
+ </p>
+ </div>
+ </form>
+ <div class="hint">
+ <p><Translate>
+ A this point, a <b>real</b> bank would ask for an additional
+ authentication proof (PIN/TAN, one time password, ..), instead
+ of a simple calculation.
+ </Translate></p>
+ </div>
+ </div>
+ </article>
+ </Fragment>);
+}
+
+function QrCodeSection({ talerWithdrawUri, abortButton }: { talerWithdrawUri: string, abortButton: h.JSX.Element }) {
+ const i18n = useTranslator();
+ useEffect(() => {
+ //Taler Wallet WebExtension is listening to headers response and tab updates.
+ //In the SPA there is no header response with the Taler URI so
+ //this hack manually triggers the tab update after the QR is in the DOM.
+ window.location.href = `${window.location.href.split('#')[0]}#`
+ }, [])
+
+ return <section id="main" class="content">
+ <h1 class="nav">{i18n`Charge Taler Wallet`}</h1>
+ <p>{i18n`You can use this QR code to withdraw to your mobile wallet:`}</p>
+ {QR({ text: talerWithdrawUri })}
+ <p>Click <a id="linkqr" href={talerWithdrawUri}>{i18n`this link`}</a> to open your Taler wallet!</p>
+ <br />
+ {abortButton}
+ </section>
+}
+
+/**
+ * Offer the QR code (and a clickable taler://-link) to
+ * permit the passing of exchange and reserve details to
+ * the bank. Poll the backend until such operation is done.
+ */
+function TalerWithdrawalQRCode(Props: any): VNode {
+ // turns true when the wallet POSTed the reserve details:
+ const [pageState, pageStateSetter] = useContext(PageContext);
+ const {
+ withdrawalId,
+ talerWithdrawUri,
+ accountLabel,
+ backendState } = Props;
+ const i18n = useTranslator();
+ const abortButton = <a class="pure-button" onClick={() => {
+ pageStateSetter((prevState: PageStateType) => {
+ const { withdrawalId, talerWithdrawUri, ...rest } = prevState;
+ return { ...rest, withdrawalInProgress: false };
+ })
+ }}>{i18n`Abort`}</a>
+
+ console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
+ // waiting for the wallet:
+
+ const { data, error, mutate } = useSWR(`integration-api/withdrawal-operation/${withdrawalId}`);
+
+ if (typeof error !== 'undefined') {
+ console.log(`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`, error);
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ error: i18n`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`
+ }))
+ return (<Fragment><br /><br />{abortButton}</Fragment>);
+ }
+
+ // data didn't arrive yet and wallet didn't communicate:
+ if (typeof data === 'undefined')
+ return <p>{i18n`Waiting the bank to create the operaion...`}</p>
+
+
+ /**
+ * Wallet didn't communicate withdrawal details yet:
+ */
+ console.log('withdrawal status', data);
+ if (data.aborted)
+ pageStateSetter((prevState: PageStateType) => {
+ const {
+ withdrawalId,
+ talerWithdrawUri,
+ ...rest } = prevState;
+ return {
+ ...rest,
+ withdrawalInProgress: false,
+ hasError: true,
+ error: i18n`This withdrawal was aborted!`
+ };
+ })
+
+
+ if (!data.selection_done) {
+ setTimeout(() => mutate(), 1000); // check again after 1 second.
+ return (<QrCodeSection talerWithdrawUri={talerWithdrawUri} abortButton={abortButton} />);
+ }
+ /**
+ * Wallet POSTed the withdrawal details! Ask the
+ * user to authorize the operation (here CAPTCHA).
+ */
+ return (<TalerWithdrawalConfirmationQuestion backendState={backendState} />);
+}
+
+
+
+function WalletWithdraw(Props: any): VNode {
+ const { backendState, pageStateSetter, focus } = Props;
+ const currency = useContext(CurrencyContext);
+ const i18n = useTranslator();
+ let submitAmount = '5.00';
+ const amountRegex = '^[0-9]+(\.[0-9]+)?$';
+
+ const ref = useRef<HTMLInputElement>(null)
+ useEffect(() => {
+ if (focus) ref.current?.focus();
+ }, [focus]);
+ return (
+ <div id="reserve-form"
+ class="pure-form"
+ name="tform">
+ <p>
+ <label for="withdraw-amount">{i18n`Amount to withdraw:`}</label>&nbsp;
+ <input
+ type="number"
+ ref={ref}
+ id="withdraw-amount"
+ name="withdraw-amount"
+ value={submitAmount}
+ pattern={amountRegex}
+ class="amount"
+ onChange={(e): void => {
+ // FIXME: validate using 'parseAmount()',
+ // deactivate submit button as long as
+ // amount is not valid
+ submitAmount = e.currentTarget.value;
+ }} />
+ &nbsp;
+ <input
+ type="text"
+ readonly
+ class="currency-indicator"
+ size={currency.length}
+ maxLength={currency.length}
+ tabIndex={-1} value={currency} />
+ </p>
+ <p>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary"
+ type="submit"
+ value={i18n`Withdraw`}
+ onClick={() => {
+ submitAmount = validateAmount(submitAmount);
+ /**
+ * By invalid amounts, the validator prints error messages
+ * on the console, and the browser colourizes the amount input
+ * box to indicate a error.
+ */
+ if (!submitAmount) return;
+ createWithdrawalCall(
+ `${currency}:${submitAmount}`,
+ backendState,
+ pageStateSetter
+ )
+ }} />
+ </div>
+ </p>
+ </div>
+ )
+}
+
+
+/**
+ * Let the user choose a payment option,
+ * then specify the details trigger the action.
+ */
+function PaymentOptions(Props: any): VNode {
+ const { backendState, pageStateSetter, focus } = Props;
+ const currency = useContext(CurrencyContext);
+ const i18n = useTranslator();
+
+ const [tab, setTab] = useState<'charge-wallet' | 'wire-transfer'>('charge-wallet')
+
+
+ return (<article>
+ <div class="payments">
+ <div class="tab">
+ <button class={tab === 'charge-wallet' ? 'tablinks active' : 'tablinks'}
+ onClick={(): void => { setTab('charge-wallet') }}>
+ {i18n`Charge Taler wallet`}
+ </button>
+ <button class={tab === 'wire-transfer' ? 'tablinks active' : 'tablinks'}
+ onClick={(): void => { setTab('wire-transfer') }}>
+ {i18n`Wire to bank account`}
+ </button>
+ </div>
+ {tab === 'charge-wallet' &&
+ <div id='charge-wallet' class='tabcontent active'>
+ <h3>{i18n`Charge Taler wallet`}</h3>
+ <WalletWithdraw
+ backendState={backendState}
+ focus
+ pageStateSetter={pageStateSetter} />
+ </div>
+ }
+ {tab === 'wire-transfer' &&
+ <div id='wire-transfer' class='tabcontent active'>
+ <h3>{i18n`Wire to bank account`}</h3>
+ <PaytoWireTransfer
+ backendState={backendState}
+ focus
+ pageStateSetter={pageStateSetter} />
+ </div>
+ }
+ </div>
+ </article>);
+}
+
+function RegistrationButton(Props: any): VNode {
+ const { backendStateSetter, pageStateSetter } = Props;
+ const i18n = useTranslator();
+ if (UI_ALLOW_REGISTRATIONS)
+ return (<button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={() => {
+ pageStateSetter((prevState: PageStateType) => ({ ...prevState, tryRegister: true }))
+ }}>
+ {i18n`Register`}
+ </button>);
+
+
+ return (<span />);
+
+}
+
+/**
+ * Collect and submit login data.
+ */
+function LoginForm(Props: any): VNode {
+ const { backendStateSetter, pageStateSetter } = Props;
+ const [submitData, submitDataSetter] = useCredentialsRequestType();
+ const i18n = useTranslator();
+ const ref = useRef<HTMLInputElement>(null)
+ useEffect(() => {
+ ref.current?.focus();
+ }, []);
+ return (<div class="login-div">
+ <form action="javascript:void(0);" class="login-form">
+ <div class="pure-form">
+ <h2>{i18n`Please login!`}</h2>
+ <p class="unameFieldLabel loginFieldLabel formFieldLabel"><label for="username">{i18n`Username:`}</label></p>
+ <input
+ ref={ref}
+ autoFocus
+ type="text"
+ name="username"
+ id="username"
+ value={submitData && submitData.username}
+ placeholder="Username"
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ username: e.currentTarget.value,
+ }))
+ }}
+ />
+ <p class="passFieldLabel loginFieldLabel formFieldLabel"><label for="password">{i18n`Password:`}</label></p>
+ <input
+ type="password"
+ name="password"
+ id="password"
+ value={submitData && submitData.password}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ password: e.currentTarget.value,
+ }))
+ }} />
+ <br />
+ <button
+ type="submit"
+ class="pure-button pure-button-primary"
+ onClick={() => {
+ if (typeof submitData === 'undefined') {
+ console.log('login data is undefined', submitData);
+ return;
+ }
+ if (submitData.password.length == 0 || submitData.username.length == 0) {
+ console.log('username or password is the empty string', submitData);
+ return;
+ }
+ loginCall(
+ // Deep copy, to avoid the cleanup
+ // below make data disappear.
+ { ...submitData },
+ backendStateSetter,
+ pageStateSetter
+ );
+ submitDataSetter(undefined);
+ }}>{i18n`Login`}
+ </button>
+ {RegistrationButton(Props)}
+ </div>
+ </form>
+ </div>);
+}
+
+/**
+ * Collect and submit registration data.
+ */
+function RegistrationForm(Props: any): VNode {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [pageState, pageStateSetter] = useContext(PageContext);
+ const [submitData, submitDataSetter] = useCredentialsRequestType();
+ const i18n = useTranslator();
+ // https://stackoverflow.com/questions/36683770/how-to-get-the-value-of-an-input-field-using-reactjs
+ return (
+ <Fragment>
+ <h1 class="nav">
+ {
+ i18n`Welcome to ${UI_BANK_NAME}!`
+ }
+ </h1>
+ <article>
+ <div class="register-div">
+ <form action="javascript:void(0);" class="register-form">
+ <div class="pure-form">
+ <h2>{i18n`Please register!`}</h2>
+ <p class="unameFieldLabel registerFieldLabel formFieldLabel"><label for="register-un">{i18n`Username:`}</label></p>
+ <input
+ id="register-un"
+ name="register-un"
+ type="text"
+ placeholder="Username"
+ value={submitData && submitData.username}
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ username: e.currentTarget.value,
+ }))
+ }} />
+ <br />
+ <p class="unameFieldLabel registerFieldLabel formFieldLabel"><label for="register-pw">{i18n`Password:`}</label></p>
+ <input
+ type="password"
+ name="register-pw"
+ id="register-pw"
+ placeholder="Password"
+ value={submitData && submitData.password}
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ password: e.currentTarget.value,
+ }))
+ }} />
+ <br />
+ {/*
+ <label for="phone">{i18n`Phone number:`}</label>
+ // FIXME: add input validation (must start with +, otherwise only numbers)
+ <input
+ name="phone"
+ id="phone"
+ type="phone"
+ placeholder="+CC-123456789"
+ value={submitData && submitData.phone}
+ required
+ onInput={(e): void => {
+ submitDataSetter((submitData: any) => ({
+ ...submitData,
+ phone: e.currentTarget.value,
+ }))}} />
+ <br />
+ */}
+ <button
+ class="pure-button pure-button-primary btn-register"
+ onClick={() => {
+ console.log('maybe submitting the registration..');
+ console.log(submitData);
+ if (typeof submitData === 'undefined') {
+ console.log(`submit data ${submitData} is undefined`);
+ return;
+ }
+ if ((typeof submitData.password === 'undefined') ||
+ (typeof submitData.username === 'undefined')) {
+ console.log('username or password is undefined');
+ return;
+ }
+ if (submitData.password.length === 0 ||
+ submitData.username.length === 0) {
+ console.log('username or password are the empty string');
+ return;
+ }
+ console.log('submitting the registration..');
+ registrationCall(
+ { ...submitData },
+ Props.backendStateSetter, // will store BE URL, if OK.
+ pageStateSetter
+ );
+ console.log('Clearing the input data');
+ /**
+ * FIXME: clearing the data should be done by setting
+ * it to undefined, instead of the empty strings, just
+ * like done in the login function. Now set to the empty
+ * strings due to a non lively update of the <input> fields
+ * after setting to undefined.
+ */
+ submitDataSetter({ username: '', password: '' })
+ }}>
+ {i18n`Register`}
+ </button>
+ {/* FIXME: should use a different color */}
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={() => {
+ pageStateSetter((prevState: PageStateType) => ({ ...prevState, tryRegister: false }))
+ }}>
+ {i18n`Cancel`}
+ </button>
+ </div>
+ </form>
+ </div>
+ </article>
+ </Fragment>
+ )
+}
+
+/**
+ * Show one page of transactions.
+ */
+function Transactions(Props: any): VNode {
+ const { pageNumber, accountLabel } = Props;
+ const i18n = useTranslator();
+ const { data, error } = useSWR(
+ `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`
+ );
+ if (typeof error !== 'undefined') {
+ console.log('transactions not found error', error);
+ switch (error.status) {
+ case 404: {
+ return <p>Transactions page {pageNumber} was not found.</p>
+ }
+ case 401: {
+ return <p>Wrong credentials given.</p>
+ }
+ default: {
+ return <p>Transaction page {pageNumber} could not be retrieved.</p>
+ }
+ }
+ }
+ if (!data) {
+ console.log(`History data of ${accountLabel} not arrived`);
+ return <p>"Transactions page loading..."</p>;
+ }
+ console.log(`History data of ${accountLabel}`, data);
+ return (<div class="results">
+ <table class="pure-table pure-table-striped">
+ <thead>
+ <tr>
+ <th>{i18n`Date`}</th>
+ <th>{i18n`Amount`}</th>
+ <th>{i18n`Counterpart`}</th>
+ <th>{i18n`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {data.transactions.map((item: any, idx: number) => {
+ const sign = item.direction == 'DBIT' ? '-' : '';
+ const counterpart = item.direction == 'DBIT' ? item.creditorIban : item.debtorIban;
+ // Pattern:
+ //
+ // DD/MM YYYY subject -5 EUR
+ // DD/MM YYYY subject 5 EUR
+ const dateRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{1,2})/
+ const dateParse = dateRegex.exec(item.date)
+ const date = dateParse !== null ? `${dateParse[3]}/${dateParse[2]} ${dateParse[1]}` : 'date not found'
+ return (<tr key={idx}>
+ <td>{date}</td>
+ <td>{sign}{item.amount} {item.currency}</td>
+ <td>{counterpart}</td>
+ <td>{item.subject}</td>
+ </tr>);
+ })}
+ </tbody>
+ </table>
+ </div>);
+}
+
+/**
+ * Show only the account's balance. NOTE: the backend state
+ * is mostly needed to provide the user's credentials to POST
+ * to the bank.
+ */
+function Account(Props: any): VNode {
+ const { cache } = useSWRConfig();
+ const { accountLabel, backendState } = Props;
+ // Getting the bank account balance:
+ const endpoint = `access-api/accounts/${accountLabel}`;
+ const { data, error } = useSWR(endpoint);
+ const [pageState, pageStateSetter] = useContext(PageContext);
+ const {
+ withdrawalInProgress,
+ withdrawalId,
+ isLoggedIn,
+ talerWithdrawUri } = pageState;
+ const i18n = useTranslator();
+ /**
+ * This part shows a list of transactions: with 5 elements by
+ * default and offers a "load more" button.
+ */
+ const [txPageNumber, setTxPageNumber] = useTransactionPageNumber()
+ const txsPages = []
+ for (let i = 0; i <= txPageNumber; i++)
+ txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />)
+
+ if (typeof error !== 'undefined') {
+ console.log('account error', error);
+ /**
+ * FIXME: to minimize the code, try only one invocation
+ * of pageStateSetter, after having decided the error
+ * message in the case-branch.
+ */
+ switch (error.status) {
+ case 404: {
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ isLoggedIn: false,
+ error: i18n`Username or account label '${accountLabel}' not found. Won't login.`
+ }));
+
+ /**
+ * 404 should never stick to the cache, because they
+ * taint successful future registrations. How? After
+ * registering, the user gets navigated to this page,
+ * therefore a previous 404 on this SWR key (the requested
+ * resource) would still appear as valid and cause this
+ * page not to be shown! A typical case is an attempted
+ * login of a unregistered user X, and then a registration
+ * attempt of the same user X: in this case, the failed
+ * login would cache a 404 error to X's profile, resulting
+ * in the legitimate request after the registration to still
+ * be flagged as 404. Clearing the cache should prevent
+ * this. */
+ (cache as any).clear();
+ return <p>Profile not found...</p>;
+ }
+ case 401: {
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ isLoggedIn: false,
+ error: i18n`Wrong credentials given.`
+ }));
+ return <p>Wrong credentials...</p>;
+ }
+ default: {
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ isLoggedIn: false,
+ error: i18n`Account information could not be retrieved.`
+ }));
+ return <p>Unknown problem...</p>;
+ }
+ }
+ }
+ if (!data) return <p>Retrieving the profile page...</p>;
+
+ /**
+ * This block shows the withdrawal QR code.
+ *
+ * A withdrawal operation replaces everything in the page and
+ * (ToDo:) starts polling the backend until either the wallet
+ * selected a exchange and reserve public key, or a error / abort
+ * happened.
+ *
+ * After reaching one of the above states, the user should be
+ * brought to this ("Account") page where they get informed about
+ * the outcome.
+ */
+ console.log(`maybe new withdrawal ${talerWithdrawUri}`);
+ if (talerWithdrawUri) {
+ console.log('Bank created a new Taler withdrawal');
+ return (
+ <BankFrame>
+ <TalerWithdrawalQRCode
+ accountLabel={accountLabel}
+ backendState={backendState}
+ withdrawalId={withdrawalId}
+ talerWithdrawUri={talerWithdrawUri} />
+ </BankFrame>
+ );
+ }
+ const balance = parseAmount(data.balance.amount)
+
+ return (<BankFrame>
+ <div>
+ <h1 class="nav welcome-text">
+ <Translate>Welcome, {accountLabel} ({getIbanFromPayto(data.paytoUri)})!</Translate>
+ </h1>
+ </div>
+ <section id="assets">
+ <div class="asset-summary">
+ <h2>{i18n`Bank account balance`}</h2>
+ {data.balance.credit_debit_indicator == 'debit' ? (<b>-</b>) : null}
+ <div class="large-amount amount"><span class="value">{`${balance.value}`}</span>&nbsp;<span class="currency">{`${balance.currency}`}</span></div>
+ </div>
+ </section>
+ <section id="payments">
+ <div class="payments">
+ <h2>{i18n`Payments`}</h2>
+ {/* FIXME: turn into button! */}
+ <CurrencyContext.Provider value={balance.currency}>
+ {Props.children}
+ <PaymentOptions
+ backendState={backendState}
+ pageStateSetter={pageStateSetter} />
+ </CurrencyContext.Provider>
+ </div>
+ </section>
+ <section id="main">
+ <article>
+ <h2>{i18n`Latest transactions:`}</h2>
+ <Transactions pageNumber="0" accountLabel={accountLabel} />
+ </article>
+ </section>
+ </BankFrame>);
+}
+
+/**
+ * Factor out login credentials.
+ */
+function SWRWithCredentials(props: any): VNode {
+ const { username, password, backendUrl } = props;
+ const headers = new Headers();
+ headers.append(
+ 'Authorization',
+ `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
+ );
+ console.log('Likely backend base URL', backendUrl);
+ return (
+ <SWRConfig
+ value={{
+ fetcher: (url: string) =>
+ fetch(backendUrl + url || '', { headers }).then(
+ (r) => {
+ if (!r.ok)
+ throw { status: r.status, json: r.json() };
+
+ return r.json()
+ }
+ ),
+ }}>{props.children}</SWRConfig>
+ );
+}
+
+function SWRWithoutCredentials(Props: any): VNode {
+ const { baseUrl } = Props;
+ console.log('Base URL', baseUrl);
+ return (
+ <SWRConfig
+ value={{
+ fetcher: (url: string) =>
+ fetch(baseUrl + url || '').then(
+ (r) => {
+ if (!r.ok)
+ throw { status: r.status, json: r.json() };
+
+ return r.json()
+ }
+ ),
+ }}>{Props.children}</SWRConfig>
+ );
+}
+
+/**
+ * Show histories of public accounts.
+ */
+function PublicHistories(Props: any): VNode {
+ const [showAccount, setShowAccount] = useShowPublicAccount();
+ const { data, error } = useSWR('access-api/public-accounts');
+ const i18n = useTranslator();
+
+ if (typeof error !== 'undefined') {
+ console.log('account error', error);
+ switch (error.status) {
+ case 404:
+ console.log('public accounts: 404', error);
+ Props.pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ showPublicHistories: false,
+ error: i18n`List of public accounts was not found.`
+ }));
+ break;
+ default:
+ console.log('public accounts: non-404 error', error);
+ Props.pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ hasError: true,
+ showPublicHistories: false,
+ error: i18n`List of public accounts could not be retrieved.`
+ }));
+ break;
+ }
+ }
+ if (!data)
+ return (<p>Waiting public accounts list...</p>)
+ const txs: any = {};
+ const accountsBar = [];
+
+ /**
+ * Show the account specified in the props, or just one
+ * from the list if that's not given.
+ */
+ if (typeof showAccount === 'undefined' && data.publicAccounts.length > 0)
+ setShowAccount(data.publicAccounts[1].accountLabel);
+ console.log(`Public history tab: ${showAccount}`);
+
+ // Ask story of all the public accounts.
+ for (const account of data.publicAccounts) {
+ console.log('Asking transactions for', account.accountLabel)
+ const isSelected = account.accountLabel == showAccount;
+ accountsBar.push(
+ <li class={isSelected ? 'pure-menu-selected pure-menu-item' : 'pure-menu-item pure-menu'}>
+ <a href="#"
+ class="pure-menu-link"
+ onClick={() => setShowAccount(account.accountLabel)}>{account.accountLabel}</a>
+ </li>
+ );
+ txs[account.accountLabel] = <Transactions accountLabel={account.accountLabel} pageNumber={0} />
+ }
+
+ return (<Fragment>
+ <h1 class="nav">{i18n`History of public accounts`}</h1>
+ <section id="main">
+ <article>
+ <div class="pure-menu pure-menu-horizontal" name="accountMenu">
+ <ul class="pure-menu-list">{accountsBar}</ul>
+ {typeof showAccount !== 'undefined' ? txs[showAccount] : <p>No public transactions found.</p>}
+ {Props.children}
+ </div>
+ </article>
+ </section>
+ </Fragment>);
+}
+
+/**
+ * If the user is logged in, it displays
+ * the balance, otherwise it offers to login.
+ */
+export function BankHome(): VNode {
+ const [backendState, backendStateSetter] = useBackendState();
+ const [pageState, pageStateSetter] = usePageState();
+ const [accountState, accountStateSetter] = useAccountState();
+ const setTxPageNumber = useTransactionPageNumber()[1];
+ const i18n = useTranslator();
+
+ if (pageState.showPublicHistories)
+ return (<SWRWithoutCredentials baseUrl={getRootPath()}>
+ <PageContext.Provider value={[pageState, pageStateSetter]}>
+ <BankFrame>
+ <PublicHistories pageStateSetter={pageStateSetter}>
+ <br />
+ <a class="pure-button" onClick={() => {
+ pageStateSetter((prevState: PageStateType) =>
+ ({ ...prevState, showPublicHistories: false }))
+ }}>Go back</a>
+ </PublicHistories>
+ </BankFrame>
+ </PageContext.Provider>
+ </SWRWithoutCredentials>);
+
+ if (pageState.tryRegister) {
+ console.log('allow registrations?', UI_ALLOW_REGISTRATIONS);
+ if (UI_ALLOW_REGISTRATIONS)
+ return (
+ <PageContext.Provider value={[pageState, pageStateSetter]}>
+ <BankFrame>
+ <RegistrationForm backendStateSetter={backendStateSetter} />
+ </BankFrame>
+ </PageContext.Provider>
+ );
+
+ return (
+ <PageContext.Provider value={[pageState, pageStateSetter]}>
+ <BankFrame>
+ <p>{i18n`Currently, the bank is not accepting new registrations!`}</p>
+ </BankFrame>
+ </PageContext.Provider>
+ );
+ }
+ if (pageState.isLoggedIn) {
+ if (typeof backendState === 'undefined') {
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ hasError: true,
+ isLoggedIn: false,
+ error: i18n`Page has a problem: logged in but backend state is lost.`
+ }));
+ return (<p>Error: waiting for details...</p>);
+ }
+ console.log('Showing the profile page..');
+ return (
+ <SWRWithCredentials
+ username={backendState.username}
+ password={backendState.password}
+ backendUrl={backendState.url}>
+ <PageContext.Provider value={[pageState, pageStateSetter]}>
+ <Account accountLabel={backendState.username} backendState={backendState} />
+ </PageContext.Provider>
+ </SWRWithCredentials>
+ );
+ } // end of logged-in state.
+
+ return (
+ <PageContext.Provider value={[pageState, pageStateSetter]}>
+ <BankFrame>
+ <h1 class="nav">
+ {
+ i18n`Welcome to ${UI_BANK_NAME}!`
+ }
+ </h1>
+ <LoginForm
+ pageStateSetter={pageStateSetter}
+ backendStateSetter={backendStateSetter} />
+ </BankFrame>
+ </PageContext.Provider>
+ );
+}