diff options
18 files changed, 669 insertions, 399 deletions
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 { <tr> <th>{i18n.str`Created`}</th> <th>{i18n.str`Confirmed`}</th> - <th>{i18n.str`Counterpart`}</th> + <th>{i18n.str`Status`}</th> <th>{i18n.str`Subject`}</th> </tr> </thead> @@ -53,8 +53,9 @@ export function ReadyView({ cashouts }: State.Ready): VNode { ? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss") : "-"} </td> + <td>{Amounts.stringifyValue(item.amount_debit)}</td> <td>{Amounts.stringifyValue(item.amount_credit)}</td> - <td>{item.counterpart}</td> + <td>{item.status}</td> <td>{item.subject}</td> </tr> ); 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 <http://www.gnu.org/licenses/> */ -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<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>> => { - const res = await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(`access-api/accounts/${account}/withdrawals`, { - method: "POST", - data, - contentType: "json" - }); + ): Promise< + HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse> + > => { + const res = + await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>( + `access-api/accounts/${account}/withdrawals`, + { + method: "POST", + data, + contentType: "json", + }, + ); return res; }; - const abortWithdrawal = async ( - id: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { - method: "POST", - contentType: "json" - }); + const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => { + const res = await request<void>( + `access-api/accounts/${account}/withdrawals/${id}`, + { + method: "POST", + contentType: "json", + }, + ); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); return res; }; const confirmWithdrawal = async ( id: string, ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { - method: "POST", - contentType: "json" - }); + const res = await request<void>( + `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<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/accounts/${account}/transactions`, { - method: "POST", - data, - contentType: "json" - }); + const res = await request<void>( + `access-api/accounts/${account}/transactions`, + { + method: "POST", + data, + contentType: "json", + }, + ); await mutateAll(/.*accounts\/.*\/transactions.*/); return res; }; - const deleteAccount = async ( - ): Promise<HttpResponseOk<void>> => { + const deleteAccount = async (): Promise<HttpResponseOk<void>> => { const res = await request<void>(`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<HttpResponseOk<void>> => { const res = await noAuthRequest<void>(`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<HttpResponseOk<void>>; } export interface AccessAPI { createWithdrawal: ( data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, - ) => Promise<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>>; - abortWithdrawal: ( - wid: string, - ) => Promise<HttpResponseOk<void>>; - confirmWithdrawal: ( - wid: string - ) => Promise<HttpResponseOk<void>>; + ) => Promise< + HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse> + >; + abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; + confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>; createTransaction: ( - data: SandboxBackend.Access.CreateBankAccountTransactionCreate + data: SandboxBackend.Access.CreateBankAccountTransactionCreate, ) => Promise<HttpResponseOk<void>>; deleteAccount: () => Promise<HttpResponseOk<void>>; } @@ -135,13 +154,17 @@ export interface InstanceTemplateFilter { position?: string; } - -export function useAccountDetails(account: string): HttpResponse<SandboxBackend.Access.BankAccountBalanceResponse, SandboxBackend.SandboxError> { +export function useAccountDetails( + account: string, +): HttpResponse< + SandboxBackend.Access.BankAccountBalanceResponse, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>, - HttpError<SandboxBackend.SandboxError> + RequestError<SandboxBackend.SandboxError> >([`access-api/accounts/${account}`], fetcher, { refreshInterval: 0, refreshWhenHidden: false, @@ -155,17 +178,23 @@ export function useAccountDetails(account: string): HttpResponse<SandboxBackend. }); if (data) return data; - if (error) return error; + if (error) return error.info; return { loading: true }; } // FIXME: should poll -export function useWithdrawalDetails(account: string, wid: string): HttpResponse<SandboxBackend.Access.BankAccountGetWithdrawalResponse, SandboxBackend.SandboxError> { +export function useWithdrawalDetails( + account: string, + wid: string, +): HttpResponse< + SandboxBackend.Access.BankAccountGetWithdrawalResponse, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>, - HttpError<SandboxBackend.SandboxError> + RequestError<SandboxBackend.SandboxError> >([`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<SandboxBackend.Access.BankAccountTransactionInfo, SandboxBackend.SandboxError> { +export function useTransactionDetails( + account: string, + tid: string, +): HttpResponse< + SandboxBackend.Access.BankAccountTransactionInfo, + SandboxBackend.SandboxError +> { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>, - HttpError<SandboxBackend.SandboxError> + RequestError<SandboxBackend.SandboxError> >([`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<SandboxBackend.Access.PublicAccountsResponse, SandboxBackend.SandboxError> { +): 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<SandboxBackend.Access.PublicAccountsResponse>, - HttpError<SandboxBackend.SandboxError> + RequestError<SandboxBackend.SandboxError> >([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher); const [lastAfter, setLastAfter] = useState< - HttpResponse<SandboxBackend.Access.PublicAccountsResponse, SandboxBackend.SandboxError> + 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<SandboxBackend.Access.BankAccountTransactionsResponse, SandboxBackend.SandboxError> { +): 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<SandboxBackend.Access.BankAccountTransactionsResponse>, - HttpError<SandboxBackend.SandboxError> - >([`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], paginatedFetcher); + RequestError<SandboxBackend.SandboxError> + >( + [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], + paginatedFetcher, + ); const [lastAfter, setLastAfter] = useState< - HttpResponse<SandboxBackend.Access.BankAccountTransactionsResponse, SandboxBackend.SandboxError> + 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<HttpResponseOk<T>>; fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>[]>; - paginatedFetcher: <T>(args: [string, number, number]) => Promise<HttpResponseOk<T>>; - sandboxAccountsFetcher: <T>(args: [string, number, number, string]) => Promise<HttpResponseOk<T>>; + multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>; + paginatedFetcher: <T>( + args: [string, number, number], + ) => Promise<HttpResponseOk<T>>; + sandboxAccountsFetcher: <T>( + args: [string, number, number, string], + ) => Promise<HttpResponseOk<T>>; } - - export function usePublicBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const baseUrl = state.url + const baseUrl = state.url; const request = useCallback( function requestImpl<T>( path: string, options: RequestOptions = {}, ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, path, options); }, [baseUrl], @@ -133,15 +142,21 @@ export function usePublicBackend(): useBackendType { [baseUrl], ); const paginatedFetcher = useCallback( - function fetcherImpl<T>([endpoint, page, size]: [string, number, number]): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); + function fetcherImpl<T>([endpoint, page, size]: [ + string, + number, + number, + ]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + params: { page: page || 1, size }, + }); }, [baseUrl], ); const multiFetcher = useCallback( - function multiFetcherImpl<T>( - endpoints: string[], - ): Promise<HttpResponseOk<T>[]> { + function multiFetcherImpl<T>([endpoints]: string[][]): Promise< + HttpResponseOk<T>[] + > { return Promise.all( endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)), ); @@ -149,27 +164,39 @@ export function usePublicBackend(): useBackendType { [baseUrl], ); const sandboxAccountsFetcher = useCallback( - function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); + function fetcherImpl<T>([endpoint, page, size, account]: [ + string, + number, + number, + string, + ]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(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<T>( path: string, options: RequestOptions = {}, ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options }); }, [baseUrl, creds], @@ -182,36 +209,66 @@ export function useAuthenticatedBackend(): useBackendType { [baseUrl, creds], ); const paginatedFetcher = useCallback( - function fetcherImpl<T>([endpoint, page = 0, size]: [string, number, number]): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page, size } }); + function fetcherImpl<T>([endpoint, page = 0, size]: [ + string, + number, + number, + ]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(baseUrl, endpoint, { + basicAuth: creds, + params: { page, size }, + }); }, [baseUrl, creds], ); const multiFetcher = useCallback( - function multiFetcherImpl<T>( - endpoints: string[], - ): Promise<HttpResponseOk<T>[]> { + function multiFetcherImpl<T>([endpoints]: string[][]): Promise< + HttpResponseOk<T>[] + > { + console.log("list size", endpoints.length, endpoints); return Promise.all( - endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { basicAuth: creds })), + endpoints.map((endpoint) => + requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }), + ), ); }, [baseUrl, creds], ); const sandboxAccountsFetcher = useCallback( - function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size } }); + function fetcherImpl<T>([endpoint, page, size, account]: [ + string, + number, + number, + string, + ]): Promise<HttpResponseOk<T>> { + return requestHandler<T>(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<SandboxBackend.Config, SandboxBackend.SandboxError> { +export function useBackendConfig(): HttpResponse< + SandboxBackend.Config, + SandboxBackend.SandboxError +> { const { request } = usePublicBackend(); type Type = SandboxBackend.Config; - const [result, setResult] = useState<HttpResponse<Type, SandboxBackend.SandboxError>>({ loading: true }); + const [result, setResult] = useState< + HttpResponse<Type, SandboxBackend.SandboxError> + >({ loading: true }); useEffect(() => { request<Type>(`/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<void>(`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<void>(`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<HttpResponseOk<void>> => { const res = await request<void>(`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<void>(`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<void>(`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<void>(`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<HttpResponseOk<void>>; changePassword: ( account: string, - data: SandboxBackend.Circuit.AccountPasswordChange + data: SandboxBackend.Circuit.AccountPasswordChange, ) => Promise<HttpResponseOk<void>>; } export interface CircuitAccountAPI { updateAccount: ( - data: SandboxBackend.Circuit.CircuitAccountReconfiguration + data: SandboxBackend.Circuit.CircuitAccountReconfiguration, ) => Promise<HttpResponseOk<void>>; changePassword: ( - data: SandboxBackend.Circuit.AccountPasswordChange + data: SandboxBackend.Circuit.AccountPasswordChange, ) => Promise<HttpResponseOk<void>>; } - export interface InstanceTemplateFilter { //FIXME: add filter to the template list position?: string; } - -export function useMyAccountDetails(): HttpResponse<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> { - 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<typeof useApiContext>["request"], + url: string, + basicAuth: { username: string; password: string }, +): Promise<boolean> { + try { + const result = await request< + HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData> + >(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth }); + return result.ok; + } catch (error) { + return false; } - const { data, error } = useSWR< - HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>, - HttpError<SandboxBackend.SandboxError> - >([`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<boolean | undefined>(); + 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<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> { +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<SandboxBackend. keepPreviousData: true, }); - // if (isValidating) return { loading: true, data: data?.data }; if (data) return data; if (error) return error.info; return { loading: true }; } interface PaginationFilter { - account?: string, - page?: number, + account?: string; + page?: number; } -export function useAccounts( +export function useBusinessAccounts( args?: PaginationFilter, -): HttpResponsePaginated<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> { +): HttpResponsePaginated< + SandboxBackend.Circuit.CircuitAccounts, + SandboxBackend.SandboxError +> { const { sandboxAccountsFetcher } = useAuthenticatedBackend(); const [page, setPage] = useState(0); @@ -212,17 +235,21 @@ export function useAccounts( } = useSWR< HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, RequestError<SandboxBackend.SandboxError> - >([`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<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> @@ -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 { )} <section style={{ marginTop: "2em" }}> - <Moves account={account} /> + <div class="active"> + <h3>{i18n.str`Latest transactions`}</h3> + <Transactions account={account} /> + </div> </section> </Fragment> ); } -function Moves({ account }: { account: string }): VNode { - const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); - const { i18n } = useTranslationContext(); - return ( - <article> - <div class="payments"> - <div class="tab"> - <button - class={tab === "transactions" ? "tablinks active" : "tablinks"} - onClick={(): void => { - setTab("transactions"); - }} - > - {i18n.str`Transactions`} - </button> - <button - class={tab === "cashouts" ? "tablinks active" : "tablinks"} - onClick={(): void => { - setTab("cashouts"); - }} - > - {i18n.str`Cashouts`} - </button> - </div> - {tab === "transactions" && ( - <div class="active"> - <h3>{i18n.str`Latest transactions`}</h3> - <Transactions account={account} /> - </div> - )} - {tab === "cashouts" && ( - <div class="active"> - <h3>{i18n.str`Latest cashouts`}</h3> - <Cashouts account={account} /> - </div> - )} - </div> - </article> - ); -} +// function Moves({ account }: { account: string }): VNode { +// const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); +// const { i18n } = useTranslationContext(); +// return ( +// <article> +// <div class="payments"> +// <div class="tab"> +// <button +// class={tab === "transactions" ? "tablinks active" : "tablinks"} +// onClick={(): void => { +// setTab("transactions"); +// }} +// > +// {i18n.str`Transactions`} +// </button> +// <button +// class={tab === "cashouts" ? "tablinks active" : "tablinks"} +// onClick={(): void => { +// setTab("cashouts"); +// }} +// > +// {i18n.str`Cashouts`} +// </button> +// </div> +// {tab === "transactions" && ( +// )} +// {tab === "cashouts" && ( +// <div class="active"> +// <h3>{i18n.str`Latest cashouts`}</h3> +// <Cashouts account={account} /> +// </div> +// )} +// </div> +// </article> +// ); +// } 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 <div />; @@ -86,6 +86,10 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { <ShowAccountDetails account={showDetails} onLoadNotOk={onLoadNotOk} + onChangePassword={() => { + 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<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>(); @@ -268,7 +272,7 @@ function UpdateAccountPassword({ <div> <div> <h1 class="nav welcome-text"> - <i18n.Translate>Admin panel</i18n.Translate> + <i18n.Translate>Update password for {account}</i18n.Translate> </h1> </div> {error && ( @@ -277,10 +281,6 @@ function UpdateAccountPassword({ <form class="pure-form"> <fieldset> - <label for="username">{i18n.str`Username`}</label> - <input name="username" type="text" readOnly value={account} /> - </fieldset> - <fieldset> <label>{i18n.str`Password`}</label> <input type="password" @@ -366,7 +366,7 @@ function CreateNewAccount({ <div> <div> <h1 class="nav welcome-text"> - <i18n.Translate>Admin panel</i18n.Translate> + <i18n.Translate>New account</i18n.Translate> </h1> </div> {error && ( @@ -428,19 +428,21 @@ function CreateNewAccount({ ); } -function ShowAccountDetails({ +export function ShowAccountDetails({ account, onClear, onUpdateSuccess, onLoadNotOk, + onChangePassword, }: { onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => 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({ <div> <div> <h1 class="nav welcome-text"> - <i18n.Translate>Admin panel</i18n.Translate> + <i18n.Translate>Business account details</i18n.Translate> </h1> </div> {error && ( @@ -474,42 +476,59 @@ function ShowAccountDetails({ <p> <div style={{ display: "flex", justifyContent: "space-between" }}> <div> - <input - class="pure-button" - type="submit" - value={i18n.str`Close`} - onClick={async (e) => { - e.preventDefault(); - onClear(); - }} - /> + {onClear ? ( + <input + class="pure-button" + type="submit" + value={i18n.str`Close`} + onClick={async (e) => { + e.preventDefault(); + onClear(); + }} + /> + ) : undefined} </div> - <div> - <input - id="select-exchange" - class="pure-button pure-button-primary content" - disabled={update && !submitAccount} - type="submit" - value={update ? i18n.str`Confirm` : i18n.str`Update`} - onClick={async (e) => { - 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); + <div style={{ display: "flex" }}> + <div> + <input + id="select-exchange" + class="pure-button pure-button-primary content" + disabled={update && !submitAccount} + type="submit" + value={i18n.str`Change password`} + onClick={async (e) => { + e.preventDefault(); + onChangePassword(); + }} + /> + </div> + <div> + <input + id="select-exchange" + class="pure-button pure-button-primary content" + disabled={update && !submitAccount} + type="submit" + value={update ? i18n.str`Confirm` : i18n.str`Update`} + onClick={async (e) => { + 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); + } } - } - }} - /> + }} + /> + </div> </div> </div> </p> 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 <Fragment />; + return ( + <div class="some-space"> + <a + href="#" + class="pure-button pure-button-primary" + onClick={(e) => { + e.preventDefault(); + onClick(); + }} + >{i18n.str`Business Profile`}</a> + </div> + ); +} + 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 = ( - <div class="logout"> - <a - href="#" - class="pure-button logout-button" - onClick={() => { - pageStateSetter((prevState: PageStateType) => { - const { talerWithdrawUri, withdrawalId, ...rest } = prevState; - backend.logOut(); - return { - ...rest, - withdrawalInProgress: false, - error: undefined, - info: undefined, - isRawPayto: false, - }; - }); - }} - >{i18n.str`Logout`}</a> - </div> - ); const demo_sites = []; for (const i in bankUiSettings.demoSites) @@ -120,7 +126,36 @@ export function BankFrame({ /> )} <StatusBanner /> - {backend.state.status === "loggedIn" ? logOut : null} + {backend.state.status === "loggedIn" ? ( + <div class="top-right"> + {goToBusinessAccount ? ( + <MaybeBusinessButton + account={backend.state.username} + onClick={goToBusinessAccount} + /> + ) : undefined} + <div class="some-space"> + <a + href="#" + class="pure-button logout-button" + onClick={() => { + pageStateSetter((prevState: PageStateType) => { + const { talerWithdrawUri, withdrawalId, ...rest } = + prevState; + backend.logOut(); + return { + ...rest, + withdrawalInProgress: false, + error: undefined, + info: undefined, + isRawPayto: false, + }; + }); + }} + >{i18n.str`Logout`}</a> + </div> + </div> + ) : null} {children} </section> <section id="footer" class="footer"> diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx new file mode 100644 index 000000000..d845c2fa0 --- /dev/null +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { + HttpResponsePaginated, + useTranslationContext, +} from "@gnu-taler/web-util/lib/index.browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Cashouts } from "../components/Cashouts/index.js"; +import { useBackendContext } from "../context/backend.js"; +import { usePageContext } from "../context/pageState.js"; +import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; +import { LoginForm } from "./LoginForm.js"; + +interface Props { + onClose: () => void; + onRegister: () => void; + onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; +} +export function BusinessAccount({ + onClose, + onLoadNotOk, + onRegister, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const { pageStateSetter } = usePageContext(); + const backend = useBackendContext(); + const [updatePassword, setUpdatePassword] = useState(false); + function showInfoMessage(info: TranslatedString): void { + pageStateSetter((prev) => ({ + ...prev, + info, + })); + } + + if (backend.state.status === "loggedOut") { + return <LoginForm onRegister={onRegister} />; + } + + if (updatePassword) { + return ( + <UpdateAccountPassword + account={backend.state.username} + onLoadNotOk={onLoadNotOk} + onUpdateSuccess={() => { + showInfoMessage(i18n.str`Password changed`); + setUpdatePassword(false); + }} + onClear={() => { + setUpdatePassword(false); + }} + /> + ); + } + return ( + <div> + <ShowAccountDetails + account={backend.state.username} + onLoadNotOk={onLoadNotOk} + onUpdateSuccess={() => { + showInfoMessage(i18n.str`Account updated`); + }} + onChangePassword={() => { + setUpdatePassword(true); + }} + onClear={onClose} + /> + <section style={{ marginTop: "2em" }}> + <div class="active"> + <h3>{i18n.str`Latest cashouts`}</h3> + <Cashouts /> + </div> + </section> + </div> + ); +} diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index e60732d42..76eb8d515 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -50,6 +50,7 @@ export function HomePage({ onRegister }: { onRegister: () => void }): VNode { } function saveErrorAndLogout(error: PageStateType["error"]): void { + console.log("rrot", error); saveError(error); backend.logOut(); } @@ -123,6 +124,7 @@ function handleNotOkResult( return function handleNotOkResult2<T, E>( result: HttpResponsePaginated<T, E>, ): VNode { + console.log("qweqwe", JSON.stringify(result, undefined, 2)); if (result.clientError && result.isUnauthorized) { onErrorHandler({ title: i18n.str`Wrong credentials for "${account}"`, @@ -139,7 +141,7 @@ function handleNotOkResult( if (!result.ok) { onErrorHandler({ title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, - description: `Diagnostic from ${result.info?.url.href} is "${result.message}"`, + description: `Diagnostic from ${result.info?.url} is "${result.message}"`, debug: JSON.stringify(result.error), }); return <LoginForm onRegister={onRegister} />; diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx index a88af9b0b..cff561aac 100644 --- a/packages/demobank-ui/src/pages/Routing.tsx +++ b/packages/demobank-ui/src/pages/Routing.tsx @@ -28,6 +28,7 @@ import { HomePage } from "./HomePage.js"; import { BankFrame } from "./BankFrame.js"; import { PublicHistoriesPage } from "./PublicHistoriesPage.js"; import { RegistrationPage } from "./RegistrationPage.js"; +import { BusinessAccount } from "./BusinessAccount.js"; function handleNotOkResult( safe: string, @@ -96,7 +97,11 @@ export function Routing(): VNode { <Route path="/account" component={() => ( - <BankFrame> + <BankFrame + goToBusinessAccount={() => { + route("/business"); + }} + > <HomePage onRegister={() => { route("/register"); @@ -105,6 +110,22 @@ export function Routing(): VNode { </BankFrame> )} /> + <Route + path="/business" + component={() => ( + <BankFrame> + <BusinessAccount + onClose={() => { + route("/account"); + }} + onRegister={() => { + route("/register"); + }} + onLoadNotOk={handleNotOkResult("/account", saveError, i18n)} + /> + </BankFrame> + )} + /> <Route default component={Redirect} to="/account" /> </Router> ); diff --git a/packages/demobank-ui/src/scss/bank.scss b/packages/demobank-ui/src/scss/bank.scss index c55dfe966..2bd5f317a 100644 --- a/packages/demobank-ui/src/scss/bank.scss +++ b/packages/demobank-ui/src/scss/bank.scss @@ -51,8 +51,11 @@ input[type="number"]::-webkit-inner-spin-button { overflow: hidden; } -.logout { +.top-right { float: right; +} +.some-space { + display: inline-block; border: 20px; margin-right: 15px; margin-top: 15px; diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 0dc24e468..642b3c68d 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -54,8 +54,10 @@ export type PartialButDefined<T> = { }; export type WithIntermediate<Type extends object> = { - [prop in keyof Type]: Type[prop] extends object ? WithIntermediate<Type[prop]> : (Type[prop] | undefined); -} + [prop in keyof Type]: Type[prop] extends object + ? WithIntermediate<Type[prop]> + : Type[prop] | undefined; +}; // export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> { // const root = obj === undefined ? {} : obj; |