merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit bce078986a8c7b1f53c69a78aa9053ac271165be
parent 17b274df8d8eeadce75c410fe17defb318d383f7
Author: ms <ms@taler.net>
Date:   Tue,  7 Dec 2021 14:20:50 +0100

Hello World euFin bank.

Diffstat:
Rpackages/bank/src/components/FlieButton.tsx -> packages/bank/src/components/FileButton.tsx | 0
Mpackages/bank/src/components/app.tsx | 5+----
Mpackages/bank/src/components/menu/SideBar.tsx | 2+-
Mpackages/bank/src/index.ts | 2+-
Mpackages/bank/src/pages/home/index.tsx | 300++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 301 insertions(+), 8 deletions(-)

diff --git a/packages/bank/src/components/FlieButton.tsx b/packages/bank/src/components/FileButton.tsx diff --git a/packages/bank/src/components/app.tsx b/packages/bank/src/components/app.tsx @@ -6,10 +6,7 @@ import { Menu } from "./menu"; const App: FunctionalComponent = () => { return ( <TranslationProvider> - <div id="app" class="has-navbar-fixed-top"> - <Menu title="Bank" /> - <BankHome /> - </div> + <BankHome /> </TranslationProvider> ); }; diff --git a/packages/bank/src/components/menu/SideBar.tsx b/packages/bank/src/components/menu/SideBar.tsx @@ -37,7 +37,7 @@ export function Sidebar({ mobile }: Props): VNode { <div class="aside-tools"> <div class="aside-tools-label"> <div> - <b>GNU Taler Bank</b> + <b>euFin bank</b> </div> <div class="is-size-7 has-text-right" diff --git a/packages/bank/src/index.ts b/packages/bank/src/index.ts @@ -1,4 +1,4 @@ import App from "./components/app"; -import "./scss/main.scss"; +// import "./scss/main.scss"; export default App; diff --git a/packages/bank/src/pages/home/index.tsx b/packages/bank/src/pages/home/index.tsx @@ -1,5 +1,301 @@ -import { h } from "preact"; +import { h, Fragment } from "preact"; +import {useState} from "preact/hooks"; +import axios from "axios"; + +/********************************************** + * 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 | undefined; + password: string | undefined; +} + +/** + * Request body of /register. + */ +interface RegistrationRequestType { + username: string; + password: string; +} + +/** + * Track page state. + */ +interface PageStateType { + isLoggedIn: boolean; + hasProblem: boolean; +} + +/** + * Bank account specific information. + */ +interface AccountStateType { + balance: string | undefined; + /* FIXME: Need history here. */ + + /** + * Error message to diplay when one of the + * account state entries failed to arrive. + */ + error: string | undefined; +} + +/******************* + * State managers. * + ******************/ + +/** + * Return getters and setters for + * login credentials and backend's + * base URL. + */ +function useBackendState( + state: BackendStateType = { + url: window.location.href, // Should never change. + username: undefined, + password: undefined, +}): [ + BackendStateType, + (newState: BackendStateType) => void | + ((fn: (oldState: BackendStateType) => void) => void) +] { + return useState<BackendStateType>(state); +} + +function useAccountState<AccountStateType>( + state: AccountStateType = { + balance: undefined, + error: undefined +}): [ + AccountStateType, (fn: (state: AccountStateType) => void) => void +] { + return useState<AccountStateType>(state); +} + +function usePageState( + state: PageStateType = { + isLoggedIn: false, + hasProblem: false, +}): [ + PageStateType, (newState: PageStateType) => any | + ((fn: (state: PageStateType) => void) => void) +] { + return useState<PageStateType>(state); +} + +/************ + * Helpers. * + ***********/ + +async function post(url: string, data: any): Promise<number> { + // Mock 200 OK responses, at this moment. + return new Promise( + // (res, rej) => {setTimeout(() => {res(404);});} + (res, rej) => {setTimeout(() => {res(200);});} + ); +} + +async function get(url: string): Promise<number> { + // Mock 200 OK responses, at this moment. + return new Promise( + (res, rej) => {setTimeout(() => {res(404);});} + // (res, rej) => {setTimeout(() => {res(200);});} + ); +} + +/****************** + * HTTP wrappers. * + *****************/ + +/** + * Wrappers for HTTP requests specific to individual API calls. + * Each wrapper will (1) manage HTTP requests and responses, + * and (2) update the state accordingly. + * + * Their signature is: + * RequestType x ResponseType x use*State() => void + * + * For example, a 'wrap()' function can look like: + * + * wrap(url: string, + * req: WrapTypeReq, + * state: StateType // Will only have setters used. + * ) { + * + * let cb: (resp: WrapTypeRes) => void = { + * // implementation here. + * // .. + * state.setter(...) + * }; + * + * post(url, req, (resp) => cb(resp)) + * } + * + ***/ + + +/** + * This function requests GET /accounts/{account_name}. + * + * It's only a information retriever, without any effect + * on the state. + */ +async function accountInfoCall( + backendState: BackendStateType, + accountStateSetter: (fn: (state: AccountStateType) => void) => void +) { + const url = new URL(`accounts/${backendState.username}`, backendState.url); + const handleResp = (respStatus: number) => { + switch (respStatus) { + case 200: { + accountStateSetter(state => ({...state, balance: "1 EUR"})); + break; + } + default: { + accountStateSetter(state => ({...state, error: "Missing information."})); + } + } + }; + const resp = await get(url) + handleResp(resp) +} + +/** + * 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( + url: string, + req: RegistrationRequestType, + // On success, that will update the username and password to the state. + backendStateSetter: (fn: (state: BackendStateType) => void) => void, + // Communicate the request outcome to the page. + pageStateSetter: (fn: (state: PageStateType) => void) => void, +) { + console.log("Try to register", req); + var handleResp = (respStatus: number) => { + switch (respStatus) { + case 200: { + pageStateSetter(state => ({...state, isLoggedIn: true})); + backendStateSetter(state => ({ + ...state, + username: req.username, + password: req.password, + })); + break; + } + default: { + pageStateSetter(state => ({...state, hasProblem: true})); + } + } + } + var resp = await post(url, req); + handleResp(resp); +} + +/************************** + * Functional components. * + *************************/ + +/** + * Show only the account's balance. + */ +export function Account(props: {balance: string}) { + return <p>Your balance is {props.balance}</p>; +} + +/** + * If the user is logged in, it displays + * the balance, otherwise it offers to login. + */ export function BankHome() { - return <section class="section">hello bank</section>; + var [backendState, backendStateSetter] = useBackendState<BackendStateType>(); + var [pageState, pageStateSetter] = usePageState<PageStateType>(); + var [accountState, accountStateSetter] = useAccountState<AccountStateType>(); + + // Prepare/check registration request. + var registrationData: {}; // Untyped collector of user input. + let prepareRegistrationRequest = (): RegistrationRequestType => { + + console.log("Preparing registration request", registrationData); + if (!("username" in registrationData)) { + pageStateSetter({...pageState, hasProblem: true}); + return; + } + if (!("password" in registrationData)) { + pageStateSetter({...pageState, hasProblem: true}); + return; + } + const u: string = registrationData["username"] + const p: string = registrationData["password"] + // Here, input is valid. + return {username: u, password: p}; + } + + if (pageState.hasProblem) { + return <p>Page has a problem.</p>; + } + + if (pageState.isLoggedIn) { + if (typeof accountState.error !== "undefined") { + console.log(accountState); + return <p>The page could not load correctly: {accountState.error}</p>; + } + if (typeof accountState.balance === "undefined") { + // Need one: request and trigger new state! + accountInfoCall(backendState, accountStateSetter); + return; + } + + return <Fragment> + <p>Welcome {backendState.username}!</p> + <Account balance={accountState.balance} /> + </Fragment>; + + /** + * FIXME: need to offer a Taler withdraw button here. + */ + + /** + * FIXME: need to offer the withdraw confirmation page, + * after the wallet has 'selected' an exchange and a reserve. + * + * The selection can be detected by asking the withdrawal + * status to the bank's backend. + */ + } + + // Proceede collecting registration data. + return <div> + <input type="text" + placeholder="username" + onInput={(e): void => { + registrationData = {...registrationData, username: e.currentTarget.value}; + }} + / > + <input type="text" + placeholder="password" + onInput={(e): void => { + registrationData = {...registrationData, password: e.currentTarget.value}; + }} + / > + + <button + onClick={() => {registrationCall( + backendState.url, + prepareRegistrationRequest(), + backendStateSetter, + pageStateSetter, + )}}>Submit</button> + </div> }