From ba8b40c9150586982e42e76d364d76202627bd6f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 10 Feb 2023 09:51:37 -0300 Subject: business account --- .../demobank-ui/src/components/Cashouts/index.ts | 2 +- .../demobank-ui/src/components/Cashouts/state.ts | 17 +- .../src/components/Cashouts/stories.tsx | 18 +- .../demobank-ui/src/components/Cashouts/views.tsx | 5 +- .../src/components/Transactions/state.ts | 108 +++++------ packages/demobank-ui/src/context/pageState.ts | 3 +- packages/demobank-ui/src/declaration.d.ts | 22 +-- packages/demobank-ui/src/hooks/access.ts | 210 +++++++++++++-------- packages/demobank-ui/src/hooks/backend.ts | 125 ++++++++---- packages/demobank-ui/src/hooks/circuit.ts | 151 +++++++++------ packages/demobank-ui/src/pages/AccountPage.tsx | 81 ++++---- packages/demobank-ui/src/pages/AdminPage.tsx | 117 +++++++----- packages/demobank-ui/src/pages/BankFrame.tsx | 81 +++++--- packages/demobank-ui/src/pages/BusinessAccount.tsx | 90 +++++++++ packages/demobank-ui/src/pages/HomePage.tsx | 4 +- packages/demobank-ui/src/pages/Routing.tsx | 23 ++- packages/demobank-ui/src/scss/bank.scss | 5 +- packages/demobank-ui/src/utils.ts | 6 +- 18 files changed, 669 insertions(+), 399 deletions(-) create mode 100644 packages/demobank-ui/src/pages/BusinessAccount.tsx (limited to 'packages/demobank-ui') diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts index db39ba7e4..1410267be 100644 --- a/packages/demobank-ui/src/components/Cashouts/index.ts +++ b/packages/demobank-ui/src/components/Cashouts/index.ts @@ -23,7 +23,7 @@ import { useComponentState } from "./state.js"; import { LoadingUriView, ReadyView } from "./views.js"; export interface Props { - account: string; + empty?: boolean; } export type State = State.Loading | State.LoadingUriError | State.Ready; diff --git a/packages/demobank-ui/src/components/Cashouts/state.ts b/packages/demobank-ui/src/components/Cashouts/state.ts index 7e420940f..178a1e815 100644 --- a/packages/demobank-ui/src/components/Cashouts/state.ts +++ b/packages/demobank-ui/src/components/Cashouts/state.ts @@ -18,27 +18,24 @@ import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util"; import { useCashouts } from "../../hooks/circuit.js"; import { Props, State, Transaction } from "./index.js"; -export function useComponentState({ - account, -}: Props): State { - const result = useCashouts() +export function useComponentState({ empty }: Props): State { + const result = useCashouts(); if (result.loading) { return { status: "loading", - error: undefined - } + error: undefined, + }; } if (!result.ok) { return { status: "loading-error", - error: result - } + error: result, + }; } - return { status: "ready", error: undefined, - cashout: result.data, + cashouts: result.data, }; } diff --git a/packages/demobank-ui/src/components/Cashouts/stories.tsx b/packages/demobank-ui/src/components/Cashouts/stories.tsx index 77fdde092..05439780c 100644 --- a/packages/demobank-ui/src/components/Cashouts/stories.tsx +++ b/packages/demobank-ui/src/components/Cashouts/stories.tsx @@ -26,20 +26,4 @@ export default { title: "transaction list", }; -export const Ready = tests.createExample(ReadyView, { - transactions: [ - { - amount: { - currency: "USD", - fraction: 0, - value: 1, - }, - counterpart: "ASD", - negative: false, - subject: "Some", - when: { - t_ms: new Date().getTime(), - }, - }, - ], -}); +export const Ready = tests.createExample(ReadyView, {}); diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index 30803d4d1..16ae8a58f 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -39,7 +39,7 @@ export function ReadyView({ cashouts }: State.Ready): VNode { {i18n.str`Created`} {i18n.str`Confirmed`} - {i18n.str`Counterpart`} + {i18n.str`Status`} {i18n.str`Subject`} @@ -53,8 +53,9 @@ export function ReadyView({ cashouts }: State.Ready): VNode { ? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss") : "-"} + {Amounts.stringifyValue(item.amount_debit)} {Amounts.stringifyValue(item.amount_credit)} - {item.counterpart} + {item.status} {item.subject} ); diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts index 9e1bce39b..198ef6c5f 100644 --- a/packages/demobank-ui/src/components/Transactions/state.ts +++ b/packages/demobank-ui/src/components/Transactions/state.ts @@ -18,21 +18,19 @@ import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/access.js"; import { Props, State, Transaction } from "./index.js"; -export function useComponentState({ - account, -}: Props): State { - const result = useTransactions(account) +export function useComponentState({ account }: Props): State { + const result = useTransactions(account); if (result.loading) { return { status: "loading", - error: undefined - } + error: undefined, + }; } if (!result.ok) { return { status: "loading-error", - error: result - } + error: result, + }; } // if (error) { // switch (error.status) { @@ -73,53 +71,57 @@ export function useComponentState({ // }; // } - const transactions = result.data.transactions.map((item: unknown) => { - if ( - !item || - typeof item !== "object" || - !("direction" in item) || - !("creditorIban" in item) || - !("debtorIban" in item) || - !("date" in item) || - !("subject" in item) || - !("currency" in item) || - !("amount" in item) - ) { - //not valid - return; - } - const anyItem = item as any; - if ( - !(typeof anyItem.creditorIban === "string") || - !(typeof anyItem.debtorIban === "string") || - !(typeof anyItem.date === "string") || - !(typeof anyItem.subject === "string") || - !(typeof anyItem.currency === "string") || - !(typeof anyItem.amount === "string") - ) { - return; - } + const transactions = result.data.transactions + .map((item: unknown) => { + if ( + !item || + typeof item !== "object" || + !("direction" in item) || + !("creditorIban" in item) || + !("debtorIban" in item) || + !("date" in item) || + !("subject" in item) || + !("currency" in item) || + !("amount" in item) + ) { + //not valid + return; + } + const anyItem = item as any; + if ( + !(typeof anyItem.creditorIban === "string") || + !(typeof anyItem.debtorIban === "string") || + !(typeof anyItem.date === "string") || + !(typeof anyItem.subject === "string") || + !(typeof anyItem.currency === "string") || + !(typeof anyItem.amount === "string") + ) { + return; + } - const negative = anyItem.direction === "DBIT"; - const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban; + const negative = anyItem.direction === "DBIT"; + const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban; - let date = anyItem.date ? parseInt(anyItem.date, 10) : 0 - if (isNaN(date) || !isFinite(date)) { - date = 0 - } - const when: AbsoluteTime = !date ? AbsoluteTime.never() : { - t_ms: date, - }; - const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`); - const subject = anyItem.subject; - return { - negative, - counterpart, - when, - amount, - subject, - }; - }).filter((x): x is Transaction => x !== undefined); + let date = anyItem.date ? parseInt(anyItem.date, 10) : 0; + if (isNaN(date) || !isFinite(date)) { + date = 0; + } + const when: AbsoluteTime = !date + ? AbsoluteTime.never() + : { + t_ms: date, + }; + const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`); + const subject = anyItem.subject; + return { + negative, + counterpart, + when, + amount, + subject, + }; + }) + .filter((x): x is Transaction => x !== undefined); return { status: "ready", diff --git a/packages/demobank-ui/src/context/pageState.ts b/packages/demobank-ui/src/context/pageState.ts index d5428b9b7..247297c7b 100644 --- a/packages/demobank-ui/src/context/pageState.ts +++ b/packages/demobank-ui/src/context/pageState.ts @@ -95,7 +95,7 @@ export type ErrorMessage = { description?: string; title: TranslatedString; debug?: string; -} +}; /** * Track page state. */ @@ -110,5 +110,4 @@ export interface PageStateType { * be moved in a future "withdrawal state" object. */ withdrawalId?: string; - } diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index cf3eb5774..c46fcc9ed 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -70,7 +70,6 @@ interface WireTransferRequestType { amount?: string; } - type HashCode = string; type EddsaPublicKey = string; type EddsaSignature = string; @@ -101,7 +100,6 @@ type UUID = string; type Integer = number; namespace SandboxBackend { - export interface Config { // Name of this API, always "circuit". name: string; @@ -126,7 +124,6 @@ namespace SandboxBackend { error: SandboxErrorDetail; } interface SandboxErrorDetail { - // String enum classifying the error. type: ErrorType; @@ -147,13 +144,12 @@ namespace SandboxBackend { * Sandbox and Nexus, therefore the actual meaning * must be carried by the error 'message' field. */ - UtilError = "util-error" + UtilError = "util-error", } namespace Access { - interface PublicAccountsResponse { - publicAccounts: PublicAccount[] + publicAccounts: PublicAccount[]; } interface PublicAccount { iban: string; @@ -213,7 +209,6 @@ namespace SandboxBackend { } interface BankAccountTransactionInfo { - creditorIban: string; creditorBic: string; // Optional creditorName: string; @@ -233,7 +228,6 @@ namespace SandboxBackend { date: string; // milliseconds since the Unix epoch } interface CreateBankAccountTransactionCreate { - // Address in the Payto format of the wire transfer receiver. // It needs at least the 'message' query string parameter. paytoUri: string; @@ -250,7 +244,6 @@ namespace SandboxBackend { password: string; } - } namespace Circuit { @@ -281,7 +274,6 @@ namespace SandboxBackend { internal_iban?: string; } interface CircuitContactData { - // E-Mail address email?: string; @@ -289,7 +281,6 @@ namespace SandboxBackend { phone?: string; } interface CircuitAccountReconfiguration { - // Addresses where to send the TAN. contact_data: CircuitContactData; @@ -300,7 +291,6 @@ namespace SandboxBackend { cashout_address: string; } interface AccountPasswordChange { - // New password. new_password: string; } @@ -314,7 +304,6 @@ namespace SandboxBackend { // Legal subject owning the account. name: string; - } interface CircuitAccountData { @@ -336,10 +325,9 @@ namespace SandboxBackend { enum TanChannel { SMS = "sms", EMAIL = "email", - FILE = "file" + FILE = "file", } interface CashoutRequest { - // Optional subject to associate to the // cashout operation. This data will appear // as the incoming wire transfer subject in @@ -370,7 +358,6 @@ namespace SandboxBackend { uuid: string; } interface CashoutConfirm { - // the TAN that confirms $cashoutId. tan: string; } @@ -398,7 +385,6 @@ namespace SandboxBackend { cashouts: string[]; } interface CashoutStatusResponse { - status: CashoutStatus; // Amount debited to the circuit bank account. amount_debit: Amount; @@ -415,7 +401,6 @@ namespace SandboxBackend { confirmation_time?: number | null; // milliseconds since the Unix epoch } enum CashoutStatus { - // The payment was initiated after a valid // TAN was received by the bank. CONFIRMED = "confirmed", @@ -425,5 +410,4 @@ namespace SandboxBackend { PENDING = "pending", } } - } diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 4d4574dac..9c162acfe 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -14,91 +14,113 @@ GNU Taler; see the file COPYING. If not, see */ -import useSWR from "swr"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; -import { useEffect, useState } from "preact/hooks"; import { - HttpError, HttpResponse, HttpResponseOk, HttpResponsePaginated, + RequestError, } from "@gnu-taler/web-util/lib/index.browser"; -import { useAuthenticatedBackend, useMatchMutate, usePublicBackend } from "./backend.js"; +import { useEffect, useState } from "preact/hooks"; +import useSWR from "swr"; import { useBackendContext } from "../context/backend.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; +import { + useAuthenticatedBackend, + useMatchMutate, + usePublicBackend, +} from "./backend.js"; export function useAccessAPI(): AccessAPI { const mutateAll = useMatchMutate(); const { request } = useAuthenticatedBackend(); - const { state } = useBackendContext() + const { state } = useBackendContext(); if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In") + throw Error("access-api can't be used when the user is not logged In"); } - const account = state.username + const account = state.username; const createWithdrawal = async ( data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, - ): Promise> => { - const res = await request(`access-api/accounts/${account}/withdrawals`, { - method: "POST", - data, - contentType: "json" - }); + ): Promise< + HttpResponseOk + > => { + const res = + await request( + `access-api/accounts/${account}/withdrawals`, + { + method: "POST", + data, + contentType: "json", + }, + ); return res; }; - const abortWithdrawal = async ( - id: string, - ): Promise> => { - const res = await request(`access-api/accounts/${account}/withdrawals/${id}`, { - method: "POST", - contentType: "json" - }); + const abortWithdrawal = async (id: string): Promise> => { + const res = await request( + `access-api/accounts/${account}/withdrawals/${id}`, + { + method: "POST", + contentType: "json", + }, + ); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); return res; }; const confirmWithdrawal = async ( id: string, ): Promise> => { - const res = await request(`access-api/accounts/${account}/withdrawals/${id}`, { - method: "POST", - contentType: "json" - }); + const res = await request( + `access-api/accounts/${account}/withdrawals/${id}`, + { + method: "POST", + contentType: "json", + }, + ); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); return res; }; const createTransaction = async ( - data: SandboxBackend.Access.CreateBankAccountTransactionCreate + data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ): Promise> => { - const res = await request(`access-api/accounts/${account}/transactions`, { - method: "POST", - data, - contentType: "json" - }); + const res = await request( + `access-api/accounts/${account}/transactions`, + { + method: "POST", + data, + contentType: "json", + }, + ); await mutateAll(/.*accounts\/.*\/transactions.*/); return res; }; - const deleteAccount = async ( - ): Promise> => { + const deleteAccount = async (): Promise> => { const res = await request(`access-api/accounts/${account}`, { method: "DELETE", - contentType: "json" + contentType: "json", }); await mutateAll(/.*accounts\/.*/); return res; }; - return { abortWithdrawal, confirmWithdrawal, createWithdrawal, createTransaction, deleteAccount }; + return { + abortWithdrawal, + confirmWithdrawal, + createWithdrawal, + createTransaction, + deleteAccount, + }; } export function useTestingAPI(): TestingAPI { const mutateAll = useMatchMutate(); const { request: noAuthRequest } = usePublicBackend(); const register = async ( - data: SandboxBackend.Access.BankRegistrationRequest + data: SandboxBackend.Access.BankRegistrationRequest, ): Promise> => { const res = await noAuthRequest(`access-api/testing/register`, { method: "POST", data, - contentType: "json" + contentType: "json", }); await mutateAll(/.*accounts\/.*/); return res; @@ -107,25 +129,22 @@ export function useTestingAPI(): TestingAPI { return { register }; } - export interface TestingAPI { register: ( - data: SandboxBackend.Access.BankRegistrationRequest + data: SandboxBackend.Access.BankRegistrationRequest, ) => Promise>; } export interface AccessAPI { createWithdrawal: ( data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, - ) => Promise>; - abortWithdrawal: ( - wid: string, - ) => Promise>; - confirmWithdrawal: ( - wid: string - ) => Promise>; + ) => Promise< + HttpResponseOk + >; + abortWithdrawal: (wid: string) => Promise>; + confirmWithdrawal: (wid: string) => Promise>; createTransaction: ( - data: SandboxBackend.Access.CreateBankAccountTransactionCreate + data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ) => Promise>; deleteAccount: () => Promise>; } @@ -135,13 +154,17 @@ export interface InstanceTemplateFilter { position?: string; } - -export function useAccountDetails(account: string): HttpResponse { +export function useAccountDetails( + account: string, +): HttpResponse< + SandboxBackend.Access.BankAccountBalanceResponse, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk, - HttpError + RequestError >([`access-api/accounts/${account}`], fetcher, { refreshInterval: 0, refreshWhenHidden: false, @@ -155,17 +178,23 @@ export function useAccountDetails(account: string): HttpResponse { +export function useWithdrawalDetails( + account: string, + wid: string, +): HttpResponse< + SandboxBackend.Access.BankAccountGetWithdrawalResponse, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk, - HttpError + RequestError >([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, { refreshInterval: 1000, refreshWhenHidden: false, @@ -176,21 +205,26 @@ export function useWithdrawalDetails(account: string, wid: string): HttpResponse errorRetryInterval: 1, shouldRetryOnError: false, keepPreviousData: true, - }); // if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error; + if (error) return error.info; return { loading: true }; } -export function useTransactionDetails(account: string, tid: string): HttpResponse { +export function useTransactionDetails( + account: string, + tid: string, +): HttpResponse< + SandboxBackend.Access.BankAccountTransactionInfo, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk, - HttpError + RequestError >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, { refreshInterval: 0, refreshWhenHidden: false, @@ -205,17 +239,20 @@ export function useTransactionDetails(account: string, tid: string): HttpRespons // if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error; + if (error) return error.info; return { loading: true }; } interface PaginationFilter { - page: number, + page: number; } export function usePublicAccounts( args?: PaginationFilter, -): HttpResponsePaginated { +): HttpResponsePaginated< + SandboxBackend.Access.PublicAccountsResponse, + SandboxBackend.SandboxError +> { const { paginatedFetcher } = usePublicBackend(); const [page, setPage] = useState(1); @@ -226,18 +263,21 @@ export function usePublicAccounts( isValidating: loadingAfter, } = useSWR< HttpResponseOk, - HttpError + RequestError >([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher); const [lastAfter, setLastAfter] = useState< - HttpResponse + HttpResponse< + SandboxBackend.Access.PublicAccountsResponse, + SandboxBackend.SandboxError + > >({ loading: true }); useEffect(() => { if (afterData) setLastAfter(afterData); }, [afterData]); - if (afterError) return afterError; + if (afterError) return afterError.info; // if the query returns less that we ask, then we have reach the end or beginning const isReachingEnd = @@ -254,30 +294,33 @@ export function usePublicAccounts( } }, loadMorePrev: () => { - null + null; }, }; - const publicAccounts = !afterData ? [] : (afterData || lastAfter).data.publicAccounts; - if (loadingAfter) - return { loading: true, data: { publicAccounts } }; + const publicAccounts = !afterData + ? [] + : (afterData || lastAfter).data.publicAccounts; + if (loadingAfter) return { loading: true, data: { publicAccounts } }; if (afterData) { return { ok: true, data: { publicAccounts }, ...pagination }; } return { loading: true }; } - /** * FIXME: mutate result when balance change (transaction ) - * @param account - * @param args - * @returns + * @param account + * @param args + * @returns */ export function useTransactions( account: string, args?: PaginationFilter, -): HttpResponsePaginated { +): HttpResponsePaginated< + SandboxBackend.Access.BankAccountTransactionsResponse, + SandboxBackend.SandboxError +> { const { paginatedFetcher } = useAuthenticatedBackend(); const [page, setPage] = useState(1); @@ -288,18 +331,24 @@ export function useTransactions( isValidating: loadingAfter, } = useSWR< HttpResponseOk, - HttpError - >([`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], paginatedFetcher); + RequestError + >( + [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], + paginatedFetcher, + ); const [lastAfter, setLastAfter] = useState< - HttpResponse + HttpResponse< + SandboxBackend.Access.BankAccountTransactionsResponse, + SandboxBackend.SandboxError + > >({ loading: true }); useEffect(() => { if (afterData) setLastAfter(afterData); }, [afterData]); - if (afterError) return afterError; + if (afterError) return afterError.info; // if the query returns less that we ask, then we have reach the end or beginning const isReachingEnd = @@ -316,13 +365,14 @@ export function useTransactions( } }, loadMorePrev: () => { - null + null; }, }; - const transactions = !afterData ? [] : (afterData || lastAfter).data.transactions; - if (loadingAfter) - return { loading: true, data: { transactions } }; + const transactions = !afterData + ? [] + : (afterData || lastAfter).data.transactions; + if (loadingAfter) return { loading: true, data: { transactions } }; if (afterData) { return { ok: true, data: { transactions }, ...pagination }; } diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index f4f5ecfd0..e87bdd5fe 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -15,7 +15,10 @@ */ import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; -import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser"; +import { + RequestError, + useLocalStorage, +} from "@gnu-taler/web-util/lib/index.browser"; import { HttpResponse, HttpResponseOk, @@ -57,7 +60,7 @@ export function getInitialBackendBaseURL(): string { export const defaultState: BackendState = { status: "loggedOut", - url: getInitialBackendBaseURL() + url: getInitialBackendBaseURL(), }; export interface BackendStateHandler { @@ -91,7 +94,12 @@ export function useBackendState(): BackendStateHandler { }, logIn(info) { //admin is defined by the username - const nextState: BackendState = { status: "loggedIn", url: state.url, ...info, isUserAdministrator: info.username === "admin" }; + const nextState: BackendState = { + status: "loggedIn", + url: state.url, + ...info, + isUserAdministrator: info.username === "admin", + }; update(JSON.stringify(nextState)); }, }; @@ -103,24 +111,25 @@ interface useBackendType { options?: RequestOptions, ) => Promise>; fetcher: (endpoint: string) => Promise>; - multiFetcher: (endpoint: string[]) => Promise[]>; - paginatedFetcher: (args: [string, number, number]) => Promise>; - sandboxAccountsFetcher: (args: [string, number, number, string]) => Promise>; + multiFetcher: (endpoint: string[][]) => Promise[]>; + paginatedFetcher: ( + args: [string, number, number], + ) => Promise>; + sandboxAccountsFetcher: ( + args: [string, number, number, string], + ) => Promise>; } - - export function usePublicBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const baseUrl = state.url + const baseUrl = state.url; const request = useCallback( function requestImpl( path: string, options: RequestOptions = {}, ): Promise> { - return requestHandler(baseUrl, path, options); }, [baseUrl], @@ -133,15 +142,21 @@ export function usePublicBackend(): useBackendType { [baseUrl], ); const paginatedFetcher = useCallback( - function fetcherImpl([endpoint, page, size]: [string, number, number]): Promise> { - return requestHandler(baseUrl, endpoint, { params: { page: page || 1, size } }); + function fetcherImpl([endpoint, page, size]: [ + string, + number, + number, + ]): Promise> { + return requestHandler(baseUrl, endpoint, { + params: { page: page || 1, size }, + }); }, [baseUrl], ); const multiFetcher = useCallback( - function multiFetcherImpl( - endpoints: string[], - ): Promise[]> { + function multiFetcherImpl([endpoints]: string[][]): Promise< + HttpResponseOk[] + > { return Promise.all( endpoints.map((endpoint) => requestHandler(baseUrl, endpoint)), ); @@ -149,27 +164,39 @@ export function usePublicBackend(): useBackendType { [baseUrl], ); const sandboxAccountsFetcher = useCallback( - function fetcherImpl([endpoint, page, size, account]: [string, number, number, string]): Promise> { - return requestHandler(baseUrl, endpoint, { params: { page: page || 1, size } }); + function fetcherImpl([endpoint, page, size, account]: [ + string, + number, + number, + string, + ]): Promise> { + return requestHandler(baseUrl, endpoint, { + params: { page: page || 1, size }, + }); }, [baseUrl], ); - return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; + return { + request, + fetcher, + paginatedFetcher, + multiFetcher, + sandboxAccountsFetcher, + }; } export function useAuthenticatedBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const creds = state.status === "loggedIn" ? state : undefined - const baseUrl = state.url + const creds = state.status === "loggedIn" ? state : undefined; + const baseUrl = state.url; const request = useCallback( function requestImpl( path: string, options: RequestOptions = {}, ): Promise> { - return requestHandler(baseUrl, path, { basicAuth: creds, ...options }); }, [baseUrl, creds], @@ -182,36 +209,66 @@ export function useAuthenticatedBackend(): useBackendType { [baseUrl, creds], ); const paginatedFetcher = useCallback( - function fetcherImpl([endpoint, page = 0, size]: [string, number, number]): Promise> { - return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page, size } }); + function fetcherImpl([endpoint, page = 0, size]: [ + string, + number, + number, + ]): Promise> { + return requestHandler(baseUrl, endpoint, { + basicAuth: creds, + params: { page, size }, + }); }, [baseUrl, creds], ); const multiFetcher = useCallback( - function multiFetcherImpl( - endpoints: string[], - ): Promise[]> { + function multiFetcherImpl([endpoints]: string[][]): Promise< + HttpResponseOk[] + > { + console.log("list size", endpoints.length, endpoints); return Promise.all( - endpoints.map((endpoint) => requestHandler(baseUrl, endpoint, { basicAuth: creds })), + endpoints.map((endpoint) => + requestHandler(baseUrl, endpoint, { basicAuth: creds }), + ), ); }, [baseUrl, creds], ); const sandboxAccountsFetcher = useCallback( - function fetcherImpl([endpoint, page, size, account]: [string, number, number, string]): Promise> { - return requestHandler(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size } }); + function fetcherImpl([endpoint, page, size, account]: [ + string, + number, + number, + string, + ]): Promise> { + return requestHandler(baseUrl, endpoint, { + basicAuth: creds, + params: { page: page || 1, size }, + }); }, [baseUrl], ); - return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; + + return { + request, + fetcher, + paginatedFetcher, + multiFetcher, + sandboxAccountsFetcher, + }; } -export function useBackendConfig(): HttpResponse { +export function useBackendConfig(): HttpResponse< + SandboxBackend.Config, + SandboxBackend.SandboxError +> { const { request } = usePublicBackend(); type Type = SandboxBackend.Config; - const [result, setResult] = useState>({ loading: true }); + const [result, setResult] = useState< + HttpResponse + >({ loading: true }); useEffect(() => { request(`/config`) @@ -238,10 +295,8 @@ export function useMatchMutate(): ( const allKeys = Array.from(cache.keys()); const keys = allKeys.filter((key) => re.test(key)); const mutations = keys.map((key) => { - mutate(key, value, true); + return mutate(key, value, true); }); return Promise.all(mutations); }; } - - diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 6e9ada601..91922a6ba 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -15,23 +15,24 @@ */ import { - HttpError, HttpResponse, HttpResponseOk, HttpResponsePaginated, - RequestError + RequestError, + useApiContext, } from "@gnu-taler/web-util/lib/index.browser"; import { useEffect, useMemo, useState } from "preact/hooks"; import useSWR from "swr"; import { useBackendContext } from "../context/backend.js"; import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; -import { useAuthenticatedBackend } from "./backend.js"; +import { useAuthenticatedBackend, useMatchMutate } from "./backend.js"; export function useAdminAccountAPI(): AdminAccountAPI { const { request } = useAuthenticatedBackend(); - const { state } = useBackendContext() + const mutateAll = useMatchMutate(); + const { state } = useBackendContext(); if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In") + throw Error("access-api can't be used when the user is not logged In"); } const createAccount = async ( @@ -40,8 +41,9 @@ export function useAdminAccountAPI(): AdminAccountAPI { const res = await request(`circuit-api/accounts`, { method: "POST", data, - contentType: "json" + contentType: "json", }); + await mutateAll(/.*circuit-api\/accounts.*/); return res; }; @@ -52,8 +54,9 @@ export function useAdminAccountAPI(): AdminAccountAPI { const res = await request(`circuit-api/accounts/${account}`, { method: "PATCH", data, - contentType: "json" + contentType: "json", }); + await mutateAll(/.*circuit-api\/accounts.*/); return res; }; const deleteAccount = async ( @@ -61,8 +64,9 @@ export function useAdminAccountAPI(): AdminAccountAPI { ): Promise> => { const res = await request(`circuit-api/accounts/${account}`, { method: "DELETE", - contentType: "json" + contentType: "json", }); + await mutateAll(/.*circuit-api\/accounts.*/); return res; }; const changePassword = async ( @@ -72,7 +76,7 @@ export function useAdminAccountAPI(): AdminAccountAPI { const res = await request(`circuit-api/accounts/${account}/auth`, { method: "PATCH", data, - contentType: "json" + contentType: "json", }); return res; }; @@ -82,9 +86,10 @@ export function useAdminAccountAPI(): AdminAccountAPI { export function useCircuitAccountAPI(): CircuitAccountAPI { const { request } = useAuthenticatedBackend(); - const { state } = useBackendContext() + const mutateAll = useMatchMutate(); + const { state } = useBackendContext(); if (state.status === "loggedOut") { - throw Error("access-api can't be used when the user is not logged In") + throw Error("access-api can't be used when the user is not logged In"); } const account = state.username; @@ -94,8 +99,9 @@ export function useCircuitAccountAPI(): CircuitAccountAPI { const res = await request(`circuit-api/accounts/${account}`, { method: "PATCH", data, - contentType: "json" + contentType: "json", }); + await mutateAll(/.*circuit-api\/accounts.*/); return res; }; const changePassword = async ( @@ -104,7 +110,7 @@ export function useCircuitAccountAPI(): CircuitAccountAPI { const res = await request(`circuit-api/accounts/${account}/auth`, { method: "PATCH", data, - contentType: "json" + contentType: "json", }); return res; }; @@ -120,57 +126,72 @@ export interface AdminAccountAPI { updateAccount: ( account: string, - data: SandboxBackend.Circuit.CircuitAccountReconfiguration + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, ) => Promise>; changePassword: ( account: string, - data: SandboxBackend.Circuit.AccountPasswordChange + data: SandboxBackend.Circuit.AccountPasswordChange, ) => Promise>; } export interface CircuitAccountAPI { updateAccount: ( - data: SandboxBackend.Circuit.CircuitAccountReconfiguration + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, ) => Promise>; changePassword: ( - data: SandboxBackend.Circuit.AccountPasswordChange + data: SandboxBackend.Circuit.AccountPasswordChange, ) => Promise>; } - export interface InstanceTemplateFilter { //FIXME: add filter to the template list position?: string; } - -export function useMyAccountDetails(): HttpResponse { - const { fetcher } = useAuthenticatedBackend(); - const { state } = useBackendContext() - if (state.status === "loggedOut") { - throw Error("can't access my-account-details when logged out") +async function getBusinessStatus( + request: ReturnType["request"], + url: string, + basicAuth: { username: string; password: string }, +): Promise { + try { + const result = await request< + HttpResponseOk + >(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth }); + return result.ok; + } catch (error) { + return false; } - const { data, error } = useSWR< - HttpResponseOk, - HttpError - >([`accounts/${state.username}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, +} + +export function useBusinessAccountFlag(): boolean | undefined { + const [isBusiness, setIsBusiness] = useState(); + const { state } = useBackendContext(); + const { request } = useApiContext(); + const creds = + state.status === "loggedOut" + ? undefined + : { username: state.username, password: state.password }; + + useEffect(() => { + if (!creds) return; + getBusinessStatus(request, state.url, creds) + .then((result) => { + setIsBusiness(result); + }) + .catch((error) => { + setIsBusiness(false); + }); }); - if (data) return data; - if (error) return error; - return { loading: true }; + return isBusiness; } -export function useAccountDetails(account: string): HttpResponse { +export function useBusinessAccountDetails( + account: string, +): HttpResponse< + SandboxBackend.Circuit.CircuitAccountData, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< @@ -188,20 +209,22 @@ export function useAccountDetails(account: string): HttpResponse { +): HttpResponsePaginated< + SandboxBackend.Circuit.CircuitAccounts, + SandboxBackend.SandboxError +> { const { sandboxAccountsFetcher } = useAuthenticatedBackend(); const [page, setPage] = useState(0); @@ -212,17 +235,21 @@ export function useAccounts( } = useSWR< HttpResponseOk, RequestError - >([`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], sandboxAccountsFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, - }); + >( + [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], + sandboxAccountsFetcher, + { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + keepPreviousData: true, + }, + ); // const [lastAfter, setLastAfter] = useState< // HttpResponse @@ -247,18 +274,18 @@ export function useAccounts( } }, loadMorePrev: () => { - null + null; }, }; const result = useMemo(() => { - const customers = !afterData ? [] : (afterData)?.data?.customers ?? []; - return { ok: true as const, data: { customers }, ...pagination } - }, [afterData?.data]) + const customers = !afterData ? [] : afterData?.data?.customers ?? []; + return { ok: true as const, data: { customers }, ...pagination }; + }, [afterData?.data]); if (afterError) return afterError.info; if (afterData) { - return result + return result; } // if (loadingAfter) diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx index 769e85804..370605871 100644 --- a/packages/demobank-ui/src/pages/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/AccountPage.tsx @@ -104,49 +104,48 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode { )}
- +
+

{i18n.str`Latest transactions`}

+ +
); } -function Moves({ account }: { account: string }): VNode { - const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); - const { i18n } = useTranslationContext(); - return ( -
-
-
- - -
- {tab === "transactions" && ( -
-

{i18n.str`Latest transactions`}

- -
- )} - {tab === "cashouts" && ( -
-

{i18n.str`Latest cashouts`}

- -
- )} -
-
- ); -} +// function Moves({ account }: { account: string }): VNode { +// const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); +// const { i18n } = useTranslationContext(); +// return ( +//
+//
+//
+// +// +//
+// {tab === "transactions" && ( +// )} +// {tab === "cashouts" && ( +//
+//

{i18n.str`Latest cashouts`}

+// +//
+// )} +//
+//
+// ); +// } diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index 9efd37f12..f8efddd80 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -24,8 +24,8 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { ErrorMessage, usePageContext } from "../context/pageState.js"; import { - useAccountDetails, - useAccounts, + useBusinessAccountDetails, + useBusinessAccounts, useAdminAccountAPI, } from "../hooks/circuit.js"; import { @@ -71,7 +71,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { })); } - const result = useAccounts({ account }); + const result = useBusinessAccounts({ account }); const { i18n } = useTranslationContext(); if (result.loading) return
; @@ -86,6 +86,10 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { { + setUpdatePassword(showDetails); + setShowDetails(undefined); + }} onUpdateSuccess={() => { showInfoMessage(i18n.str`Account updated`); setShowDetails(undefined); @@ -230,7 +234,7 @@ function initializeFromTemplate( return initial as any; } -function UpdateAccountPassword({ +export function UpdateAccountPassword({ account, onClear, onUpdateSuccess, @@ -242,7 +246,7 @@ function UpdateAccountPassword({ account: string; }): VNode { const { i18n } = useTranslationContext(); - const result = useAccountDetails(account); + const result = useBusinessAccountDetails(account); const { changePassword } = useAdminAccountAPI(); const [password, setPassword] = useState(); const [repeat, setRepeat] = useState(); @@ -268,7 +272,7 @@ function UpdateAccountPassword({

- Admin panel + Update password for {account}

{error && ( @@ -276,10 +280,6 @@ function UpdateAccountPassword({ )}
-
- - -

- Admin panel + New account

{error && ( @@ -428,19 +428,21 @@ function CreateNewAccount({ ); } -function ShowAccountDetails({ +export function ShowAccountDetails({ account, onClear, onUpdateSuccess, onLoadNotOk, + onChangePassword, }: { onLoadNotOk: (error: HttpResponsePaginated) => VNode; - onClear: () => void; + onClear?: () => void; + onChangePassword: () => void; onUpdateSuccess: () => void; account: string; }): VNode { const { i18n } = useTranslationContext(); - const result = useAccountDetails(account); + const result = useBusinessAccountDetails(account); const { updateAccount } = useAdminAccountAPI(); const [update, setUpdate] = useState(false); const [submitAccount, setSubmitAccount] = useState< @@ -459,7 +461,7 @@ function ShowAccountDetails({

- Admin panel + Business account details

{error && ( @@ -474,42 +476,59 @@ function ShowAccountDetails({

- { - e.preventDefault(); - onClear(); - }} - /> + {onClear ? ( + { + e.preventDefault(); + onClear(); + }} + /> + ) : undefined}
-
- { - e.preventDefault(); - - if (!update) { - setUpdate(true); - } else { - if (!submitAccount) return; - try { - await updateAccount(account, { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, - }); - onUpdateSuccess(); - } catch (error) { - handleError(error, saveError, i18n); +
+
+ { + e.preventDefault(); + onChangePassword(); + }} + /> +
+
+ { + e.preventDefault(); + + if (!update) { + setUpdate(true); + } else { + if (!submitAccount) return; + try { + await updateAccount(account, { + cashout_address: submitAccount.cashout_address, + contact_data: submitAccount.contact_data, + }); + onUpdateSuccess(); + } catch (error) { + handleError(error, saveError, i18n); + } } - } - }} - /> + }} + /> +

diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index ed36daa21..0fb75b87b 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -15,6 +15,7 @@ */ import { Logger } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import talerLogo from "../assets/logo-white.svg"; import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; @@ -24,41 +25,46 @@ import { PageStateType, usePageContext, } from "../context/pageState.js"; -import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; +import { useBusinessAccountDetails } from "../hooks/circuit.js"; import { bankUiSettings } from "../settings.js"; const logger = new Logger("BankFrame"); +function MaybeBusinessButton({ + account, + onClick, +}: { + account: string; + onClick: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + const result = useBusinessAccountDetails(account); + if (!result.ok) return ; + return ( + + ); +} + export function BankFrame({ children, + goToBusinessAccount, }: { children: ComponentChildren; + goToBusinessAccount?: () => void; }): VNode { const { i18n } = useTranslationContext(); const backend = useBackendContext(); const { pageState, pageStateSetter } = usePageContext(); logger.trace("state", pageState); - const logOut = ( - - ); const demo_sites = []; for (const i in bankUiSettings.demoSites) @@ -120,7 +126,36 @@ export function BankFrame({ /> )} - {backend.state.status === "loggedIn" ? logOut : null} + {backend.state.status === "loggedIn" ? ( + + ) : null} {children}