diff options
author | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
---|---|---|
committer | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
commit | fe7b51ef2736edbf04f5bbd9d19f2a2d04baccc2 (patch) | |
tree | 66c68c8d6a666f6e74dc663c9ee4f07879f6626c /packages/demobank-ui/src/hooks | |
parent | 35611f0bf9cf67638b171c2a300fab1797d3d8f0 (diff) | |
parent | 97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff) |
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages/demobank-ui/src/hooks')
-rw-r--r-- | packages/demobank-ui/src/hooks/access.ts | 131 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/backend.ts | 113 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/circuit.ts | 29 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/config.ts | 59 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/notification.ts | 54 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/settings.ts | 22 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/useCredentialsChecker.ts | 135 |
7 files changed, 357 insertions, 186 deletions
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index b8b6ab899..154c43ae6 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -44,13 +44,13 @@ export function useAccessAPI(): AccessAPI { const account = state.username; const createWithdrawal = async ( - data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, + data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest, ): Promise< - HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse> + HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse> > => { const res = - await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>( - `access-api/accounts/${account}/withdrawals`, + await request<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>( + `accounts/${account}/withdrawals`, { method: "POST", data, @@ -60,21 +60,21 @@ export function useAccessAPI(): AccessAPI { return res; }; const createTransaction = async ( - data: SandboxBackend.Access.CreateBankAccountTransactionCreate, + data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate, ): Promise<HttpResponseOk<void>> => { const res = await request<void>( - `access-api/accounts/${account}/transactions`, + `accounts/${account}/transactions`, { method: "POST", data, contentType: "json", }, ); - await mutateAll(/.*accounts\/.*\/transactions.*/); + await mutateAll(/.*accounts\/.*/); return res; }; const deleteAccount = async (): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/accounts/${account}`, { + const res = await request<void>(`accounts/${account}`, { method: "DELETE", contentType: "json", }); @@ -94,7 +94,7 @@ export function useAccessAnonAPI(): AccessAnonAPI { const { request } = useAuthenticatedBackend(); const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/withdrawals/${id}/abort`, { + const res = await request<void>(`withdrawals/${id}/abort`, { method: "POST", contentType: "json", }); @@ -104,7 +104,7 @@ export function useAccessAnonAPI(): AccessAnonAPI { const confirmWithdrawal = async ( id: string, ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`access-api/withdrawals/${id}/confirm`, { + const res = await request<void>(`withdrawals/${id}/confirm`, { method: "POST", contentType: "json", }); @@ -122,9 +122,10 @@ export function useTestingAPI(): TestingAPI { const mutateAll = useMatchMutate(); const { request: noAuthRequest } = usePublicBackend(); const register = async ( - data: SandboxBackend.Access.BankRegistrationRequest, + data: SandboxBackend.CoreBank.RegisterAccountRequest, ): Promise<HttpResponseOk<void>> => { - const res = await noAuthRequest<void>(`access-api/testing/register`, { + // FIXME: This API is deprecated. The normal account registration API should be used instead. + const res = await noAuthRequest<void>(`accounts`, { method: "POST", data, contentType: "json", @@ -138,18 +139,18 @@ export function useTestingAPI(): TestingAPI { export interface TestingAPI { register: ( - data: SandboxBackend.Access.BankRegistrationRequest, + data: SandboxBackend.CoreBank.RegisterAccountRequest, ) => Promise<HttpResponseOk<void>>; } export interface AccessAPI { createWithdrawal: ( - data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, + data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest, ) => Promise< - HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse> + HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse> >; createTransaction: ( - data: SandboxBackend.Access.CreateBankAccountTransactionCreate, + data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate, ) => Promise<HttpResponseOk<void>>; deleteAccount: () => Promise<HttpResponseOk<void>>; } @@ -166,15 +167,15 @@ export interface InstanceTemplateFilter { export function useAccountDetails( account: string, ): HttpResponse< - SandboxBackend.Access.BankAccountBalanceResponse, + SandboxBackend.CoreBank.AccountData, SandboxBackend.SandboxError > { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< - HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>, + HttpResponseOk<SandboxBackend.CoreBank.AccountData>, RequestError<SandboxBackend.SandboxError> - >([`access-api/accounts/${account}`], fetcher, { + >([`accounts/${account}`], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -186,28 +187,8 @@ export function useAccountDetails( keepPreviousData: true, }); - //FIXME: remove optional when libeufin sandbox has implemented the feature - if (data && typeof data.data.debitThreshold === "undefined") { - data.data.debitThreshold = "0"; - } - //FIXME: sandbox server should return amount string if (data) { - const isAmount = Amounts.parse(data.data.debitThreshold); - if (isAmount) { - //server response with correct format - return data; - } - const { currency } = Amounts.parseOrThrow(data.data.balance.amount); - const clone = structuredClone(data); - - const theNumber = Number.parseInt(data.data.debitThreshold, 10); - const value = Number.isNaN(theNumber) ? 0 : theNumber; - clone.data.debitThreshold = Amounts.stringify({ - currency, - value: value, - fraction: 0, - }); - return clone; + return data; } if (error) return error.cause; return { loading: true }; @@ -217,15 +198,15 @@ export function useAccountDetails( export function useWithdrawalDetails( wid: string, ): HttpResponse< - SandboxBackend.Access.BankAccountGetWithdrawalResponse, + SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse, SandboxBackend.SandboxError > { const { fetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< - HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>, + HttpResponseOk<SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse>, RequestError<SandboxBackend.SandboxError> - >([`access-api/withdrawals/${wid}`], fetcher, { + >([`withdrawals/${wid}`], fetcher, { refreshInterval: 1000, refreshWhenHidden: false, revalidateOnFocus: false, @@ -247,15 +228,15 @@ export function useTransactionDetails( account: string, tid: string, ): HttpResponse< - SandboxBackend.Access.BankAccountTransactionInfo, + SandboxBackend.CoreBank.BankAccountTransactionInfo, SandboxBackend.SandboxError > { - const { fetcher } = useAuthenticatedBackend(); + const { paginatedFetcher } = useAuthenticatedBackend(); const { data, error } = useSWR< - HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>, + HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionInfo>, RequestError<SandboxBackend.SandboxError> - >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, { + >([`accounts/${account}/transactions/${tid}`], paginatedFetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -274,13 +255,13 @@ export function useTransactionDetails( } interface PaginationFilter { - page: number; + // page: number; } export function usePublicAccounts( args?: PaginationFilter, ): HttpResponsePaginated< - SandboxBackend.Access.PublicAccountsResponse, + SandboxBackend.CoreBank.PublicAccountsResponse, SandboxBackend.SandboxError > { const { paginatedFetcher } = usePublicBackend(); @@ -292,13 +273,13 @@ export function usePublicAccounts( error: afterError, isValidating: loadingAfter, } = useSWR< - HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>, + HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>, RequestError<SandboxBackend.SandboxError> - >([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher); + >([`public-accounts`, page, PAGE_SIZE], paginatedFetcher); const [lastAfter, setLastAfter] = useState< HttpResponse< - SandboxBackend.Access.PublicAccountsResponse, + SandboxBackend.CoreBank.PublicAccountsResponse, SandboxBackend.SandboxError > >({ loading: true }); @@ -311,7 +292,7 @@ export function usePublicAccounts( // if the query returns less that we ask, then we have reach the end or beginning const isReachingEnd = - afterData && afterData.data.publicAccounts.length < PAGE_SIZE; + afterData && afterData.data.public_accounts.length < PAGE_SIZE; const isReachingStart = false; const pagination = { @@ -319,7 +300,7 @@ export function usePublicAccounts( isReachingStart, loadMore: () => { if (!afterData || isReachingEnd) return; - if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) { + if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) { setPage(page + 1); } }, @@ -328,12 +309,12 @@ export function usePublicAccounts( }, }; - const publicAccounts = !afterData + const public_accounts = !afterData ? [] - : (afterData || lastAfter).data.publicAccounts; - if (loadingAfter) return { loading: true, data: { publicAccounts } }; + : (afterData || lastAfter).data.public_accounts; + if (loadingAfter) return { loading: true, data: { public_accounts } }; if (afterData) { - return { ok: true, data: { publicAccounts }, ...pagination }; + return { ok: true, data: { public_accounts }, ...pagination }; } return { loading: true }; } @@ -348,28 +329,36 @@ export function useTransactions( account: string, args?: PaginationFilter, ): HttpResponsePaginated< - SandboxBackend.Access.BankAccountTransactionsResponse, + SandboxBackend.CoreBank.BankAccountTransactionsResponse, SandboxBackend.SandboxError > { const { paginatedFetcher } = useAuthenticatedBackend(); - const [page, setPage] = useState(1); + const [start, setStart] = useState<string>(); const { data: afterData, error: afterError, isValidating: loadingAfter, } = useSWR< - HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>, + HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>, RequestError<SandboxBackend.SandboxError> >( - [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], - paginatedFetcher, + [`accounts/${account}/transactions`, start, PAGE_SIZE], + paginatedFetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + refreshWhenOffline: false, + // revalidateOnMount: false, + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + } ); const [lastAfter, setLastAfter] = useState< HttpResponse< - SandboxBackend.Access.BankAccountTransactionsResponse, + SandboxBackend.CoreBank.BankAccountTransactionsResponse, SandboxBackend.SandboxError > >({ loading: true }); @@ -385,19 +374,23 @@ export function useTransactions( // if the query returns less that we ask, then we have reach the end or beginning const isReachingEnd = afterData && afterData.data.transactions.length < PAGE_SIZE; - const isReachingStart = false; + const isReachingStart = start == undefined; const pagination = { isReachingEnd, isReachingStart, loadMore: () => { if (!afterData || isReachingEnd) return; - if (afterData.data.transactions.length < MAX_RESULT_SIZE) { - setPage(page + 1); - } + // if (afterData.data.transactions.length < MAX_RESULT_SIZE) { + const l = afterData.data.transactions[afterData.data.transactions.length-1] + setStart(String(l.row_id)); + // } }, loadMorePrev: () => { - null; + if (!afterData || isReachingStart) return; + // if (afterData.data.transactions.length < MAX_RESULT_SIZE) { + setStart(undefined) + // } }, }; diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 4b60d1b6c..889618646 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -40,21 +40,24 @@ import { useCallback, useEffect, useState } from "preact/hooks"; import { useSWRConfig } from "swr"; import { useBackendContext } from "../context/backend.js"; import { bankUiSettings } from "../settings.js"; +import { AccessToken } from "./useCredentialsChecker.js"; /** * Has the information to reach and * authenticate at the bank's backend. */ -export type BackendState = LoggedIn | LoggedOut; +export type BackendState = LoggedIn | LoggedOut | Expired; -export interface BackendCredentials { +interface LoggedIn { + status: "loggedIn"; + isUserAdministrator: boolean; username: string; - password: string; + token: AccessToken; } - -interface LoggedIn extends BackendCredentials { - status: "loggedIn"; +interface Expired { + status: "expired"; isUserAdministrator: boolean; + username: string; } interface LoggedOut { status: "loggedOut"; @@ -64,10 +67,17 @@ export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> => buildCodecForObject<LoggedIn>() .property("status", codecForConstString("loggedIn")) .property("username", codecForString()) - .property("password", codecForString()) + .property("token", codecForString() as Codec<AccessToken>) .property("isUserAdministrator", codecForBoolean()) .build("BackendState.LoggedIn"); +export const codecForBackendStateExpired = (): Codec<Expired> => + buildCodecForObject<Expired>() + .property("status", codecForConstString("expired")) + .property("username", codecForString()) + .property("isUserAdministrator", codecForBoolean()) + .build("BackendState.Expired"); + export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> => buildCodecForObject<LoggedOut>() .property("status", codecForConstString("loggedOut")) @@ -78,6 +88,7 @@ export const codecForBackendState = (): Codec<BackendState> => .discriminateOn("status") .alternative("loggedIn", codecForBackendStateLoggedIn()) .alternative("loggedOut", codecForBackendStateLoggedOut()) + .alternative("expired", codecForBackendStateExpired()) .build("BackendState"); export function getInitialBackendBaseURL(): string { @@ -85,18 +96,27 @@ export function getInitialBackendBaseURL(): string { typeof localStorage !== "undefined" ? localStorage.getItem("bank-base-url") : undefined; + let result: string; if (!overrideUrl) { //normal path if (!bankUiSettings.backendBaseURL) { console.error( "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); - return canonicalizeBaseUrl(window.origin); + result = window.origin + } else { + result = bankUiSettings.backendBaseURL; } - return canonicalizeBaseUrl(bankUiSettings.backendBaseURL); + } else { + // testing/development path + result = overrideUrl + } + try { + return canonicalizeBaseUrl(result) + } catch (e) { + //fall back + return canonicalizeBaseUrl(window.origin) } - // testing/development path - return canonicalizeBaseUrl(overrideUrl); } export const defaultState: BackendState = { @@ -106,7 +126,8 @@ export const defaultState: BackendState = { export interface BackendStateHandler { state: BackendState; logOut(): void; - logIn(info: BackendCredentials): void; + expired(): void; + logIn(info: {username: string, token: AccessToken}): void; } const BACKEND_STATE_KEY = buildStorageKey( @@ -124,12 +145,22 @@ export function useBackendState(): BackendStateHandler { BACKEND_STATE_KEY, defaultState, ); + const mutateAll = useMatchMutate(); return { state, logOut() { update(defaultState); }, + expired() { + if (state.status === "loggedOut") return; + const nextState: BackendState = { + status: "expired", + username: state.username, + isUserAdministrator: state.username === "admin", + }; + update(nextState); + }, logIn(info) { //admin is defined by the username const nextState: BackendState = { @@ -138,6 +169,7 @@ export function useBackendState(): BackendStateHandler { isUserAdministrator: info.username === "admin", }; update(nextState); + mutateAll(/.*/) }, }; } @@ -150,7 +182,7 @@ interface useBackendType { fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>; paginatedFetcher: <T>( - args: [string, number, number], + args: [string, string | undefined, number], ) => Promise<HttpResponseOk<T>>; sandboxAccountsFetcher: <T>( args: [string, number, number, string], @@ -179,13 +211,15 @@ export function usePublicBackend(): useBackendType { [baseUrl], ); const paginatedFetcher = useCallback( - function fetcherImpl<T>([endpoint, page, size]: [ + function fetcherImpl<T>([endpoint, start, size]: [ string, - number, + string | undefined, number, ]): Promise<HttpResponseOk<T>> { + const delta = -1 * size //descending order + const params = start ? { delta, start } : { delta } return requestHandler<T>(baseUrl, endpoint, { - params: { page: page || 1, size }, + params, }); }, [baseUrl], @@ -247,35 +281,12 @@ interface InvalidationResult { error: unknown; } -export function useCredentialsChecker() { - const { request } = useApiContext(); - const baseUrl = getInitialBackendBaseURL(); - //check against account details endpoint - //while sandbox backend doesn't have a login endpoint - return async function testLogin( - username: string, - password: string, - ): Promise<CheckResult> { - try { - await request(baseUrl, `access-api/accounts/${username}/`, { - basicAuth: { username, password }, - preventCache: true, - }); - return { valid: true }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, requestError: true, cause: error.cause }; - } - return { valid: false, requestError: false, error }; - } - }; -} - export function useAuthenticatedBackend(): useBackendType { const { state } = useBackendContext(); const { request: requestHandler } = useApiContext(); - const creds = state.status === "loggedIn" ? state : undefined; + // FIXME: libeufin returns 400 insteand of 401 if there is no auth token + const creds = state.status === "loggedIn" ? state.token : "secret-token:a"; const baseUrl = getInitialBackendBaseURL(); const request = useCallback( @@ -283,26 +294,28 @@ export function useAuthenticatedBackend(): useBackendType { path: string, options: RequestOptions = {}, ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options }); + return requestHandler<T>(baseUrl, path, { token: creds, ...options }); }, [baseUrl, creds], ); const fetcher = useCallback( function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }); + return requestHandler<T>(baseUrl, endpoint, { token: creds }); }, [baseUrl, creds], ); const paginatedFetcher = useCallback( - function fetcherImpl<T>([endpoint, page = 1, size]: [ + function fetcherImpl<T>([endpoint, start, size]: [ string, - number, + string | undefined, number, ]): Promise<HttpResponseOk<T>> { + const delta = -1 * size //descending order + const params = start ? { delta, start } : { delta } return requestHandler<T>(baseUrl, endpoint, { - basicAuth: creds, - params: { page, size }, + token: creds, + params, }); }, [baseUrl, creds], @@ -313,7 +326,7 @@ export function useAuthenticatedBackend(): useBackendType { > { return Promise.all( endpoints.map((endpoint) => - requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }), + requestHandler<T>(baseUrl, endpoint, { token: creds }), ), ); }, @@ -327,7 +340,7 @@ export function useAuthenticatedBackend(): useBackendType { string, ]): Promise<HttpResponseOk<T>> { return requestHandler<T>(baseUrl, endpoint, { - basicAuth: creds, + token: creds, params: { page: page || 1, size }, }); }, @@ -339,7 +352,7 @@ export function useAuthenticatedBackend(): useBackendType { HttpResponseOk<T> > { return requestHandler<T>(baseUrl, endpoint, { - basicAuth: creds, + token: creds, params: { account }, }); }, diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 06557b77f..5dba60951 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -33,6 +33,7 @@ import { // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import _useSWR, { SWRHook } from "swr"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { AccessToken } from "./useCredentialsChecker.js"; const useSWR = _useSWR as unknown as SWRHook; export function useAdminAccountAPI(): AdminAccountAPI { @@ -90,7 +91,8 @@ export function useAdminAccountAPI(): AdminAccountAPI { await mutateAll(/.*/); logIn({ username: account, - password: data.new_password, + //FIXME: change password api + token: data.new_password as AccessToken, }); } return res; @@ -215,14 +217,15 @@ export interface CircuitAccountAPI { async function getBusinessStatus( request: ReturnType<typeof useApiContext>["request"], - basicAuth: { username: string; password: string }, + username: string, + token: AccessToken, ): Promise<boolean> { try { const url = getInitialBackendBaseURL(); const result = await request<SandboxBackend.Circuit.CircuitAccountData>( url, - `circuit-api/accounts/${basicAuth.username}`, - { basicAuth }, + `circuit-api/accounts/${username}`, + { token }, ); return result.ok; } catch (error) { @@ -264,10 +267,10 @@ type CashoutEstimators = { export function useEstimator(): CashoutEstimators { const { state } = useBackendContext(); const { request } = useApiContext(); - const basicAuth = - state.status === "loggedOut" + const creds = + state.status !== "loggedIn" ? undefined - : { username: state.username, password: state.password }; + : state.token; return { estimateByCredit: async (amount, fee, rate) => { const zeroBalance = Amounts.zeroOfCurrency(fee.currency); @@ -282,7 +285,7 @@ export function useEstimator(): CashoutEstimators { url, `circuit-api/cashouts/estimates`, { - basicAuth, + token: creds, params: { amount_credit: Amounts.stringify(amount), }, @@ -313,7 +316,7 @@ export function useEstimator(): CashoutEstimators { url, `circuit-api/cashouts/estimates`, { - basicAuth, + token: creds, params: { amount_debit: Amounts.stringify(amount), }, @@ -337,13 +340,13 @@ export function useBusinessAccountFlag(): boolean | undefined { const { state } = useBackendContext(); const { request } = useApiContext(); const creds = - state.status === "loggedOut" + state.status !== "loggedIn" ? undefined - : { username: state.username, password: state.password }; + : {user: state.username, token: state.token}; useEffect(() => { if (!creds) return; - getBusinessStatus(request, creds) + getBusinessStatus(request, creds.user, creds.token) .then((result) => { setIsBusiness(result); }) @@ -432,7 +435,7 @@ export function useBusinessAccounts( HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, RequestError<SandboxBackend.SandboxError> >( - [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], + [`accounts`, args?.page, PAGE_SIZE, args?.account], sandboxAccountsFetcher, { refreshInterval: 0, diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts new file mode 100644 index 000000000..a3bd294db --- /dev/null +++ b/packages/demobank-ui/src/hooks/config.ts @@ -0,0 +1,59 @@ +import { LibtoolVersion } from "@gnu-taler/taler-util"; +import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { getInitialBackendBaseURL } from "./backend.js"; + +/** + * Protocol version spoken with the bank. + * + * Uses libtool's current:revision:age versioning. + */ +export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; + +async function getConfigState( + request: ReturnType<typeof useApiContext>["request"], +): Promise<SandboxBackend.Config> { + const url = getInitialBackendBaseURL(); + const result = await request<SandboxBackend.Config>(url, `config`); + return result.data; +} + +export type ConfigResult = undefined + | { type: "ok", result: Required<SandboxBackend.Config> } + | { type: "wrong", result: SandboxBackend.Config } + | { type: "error", result: HttpError<SandboxBackend.SandboxError> } + +export function useConfigState(): ConfigResult { + const [checked, setChecked] = useState<ConfigResult>() + const { request } = useApiContext(); + + useEffect(() => { + getConfigState(request) + .then((result) => { + const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version) + if (r?.compatible) { + const complete: Required<SandboxBackend.Config> = { + currency_fraction_digits: result.currency_fraction_digits ?? 2, + currency_fraction_limit: result.currency_fraction_limit ?? 2, + fiat_currency: "", + have_cashout: result.have_cashout ?? false, + name: result.name, + version: result.version, + } + setChecked({ type: "ok", result: complete }); + } else { + setChecked({ type: "wrong", result }) + } + }) + .catch((error: unknown) => { + if (error instanceof RequestError) { + const result = error.cause + setChecked({ type: "error", result }); + } + }); + }, []); + + return checked; +} + + diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts deleted file mode 100644 index 9bf621b41..000000000 --- a/packages/demobank-ui/src/hooks/notification.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { memoryMap } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; - -export type NotificationMessage = ErrorNotification | InfoNotification; - -//FIXME: this should not be exported since every notification -// goes throw notify function -export interface ErrorMessage { - description?: string; - title: TranslatedString; - debug?: string; -} - -interface ErrorNotification { - type: "error"; - error: ErrorMessage; -} -interface InfoNotification { - type: "info"; - info: TranslatedString; -} - -const storage = memoryMap<NotificationMessage>(); -const NOTIFICATION_KEY = "notification"; - -export function onNotificationUpdate( - handler: (newValue: NotificationMessage | undefined) => void, -) { - return storage.onUpdate(NOTIFICATION_KEY, () => { - const newValue = storage.get(NOTIFICATION_KEY); - handler(newValue); - }); -} - -export function notifyError(error: ErrorMessage) { - storage.set(NOTIFICATION_KEY, { type: "error", error }); -} -export function notifyInfo(info: TranslatedString) { - storage.set(NOTIFICATION_KEY, { type: "info", info }); -} - -export function useNotifications(): [ - NotificationMessage | undefined, - StateUpdater<NotificationMessage | undefined>, -] { - const [value, setter] = useState<NotificationMessage | undefined>(); - useEffect(() => { - return storage.onUpdate(NOTIFICATION_KEY, () => { - setter(storage.get(NOTIFICATION_KEY)); - }); - }); - return [value, setter]; -} diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index 46b31bf2a..ad853f9d7 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -15,8 +15,12 @@ */ import { + AmountString, Codec, buildCodecForObject, + codecForAmountString, + codecForBoolean, + codecForNumber, codecForString, codecOptional, } from "@gnu-taler/taler-util"; @@ -24,15 +28,33 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; interface Settings { currentWithdrawalOperationId: string | undefined; + showWithdrawalSuccess: boolean; + showDemoDescription: boolean; + showInstallWallet: boolean; + maxWithdrawalAmount: number; + fastWithdrawal: boolean; + showDebugInfo: boolean; } export const codecForSettings = (): Codec<Settings> => buildCodecForObject<Settings>() .property("currentWithdrawalOperationId", codecOptional(codecForString())) + .property("showWithdrawalSuccess", (codecForBoolean())) + .property("showDemoDescription", (codecForBoolean())) + .property("showInstallWallet", (codecForBoolean())) + .property("fastWithdrawal", (codecForBoolean())) + .property("showDebugInfo", (codecForBoolean())) + .property("maxWithdrawalAmount", codecForNumber()) .build("Settings"); const defaultSettings: Settings = { currentWithdrawalOperationId: undefined, + showWithdrawalSuccess: true, + showDemoDescription: true, + showInstallWallet: true, + maxWithdrawalAmount: 25, + fastWithdrawal: false, + showDebugInfo: false, }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts new file mode 100644 index 000000000..b3dedb654 --- /dev/null +++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts @@ -0,0 +1,135 @@ +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; +import { getInitialBackendBaseURL } from "./backend.js"; + +export function useCredentialsChecker() { + const { request } = useApiContext(); + const baseUrl = getInitialBackendBaseURL(); + //check against instance details endpoint + //while merchant backend doesn't have a login endpoint + async function requestNewLoginToken( + username: string, + password: string, + ): Promise<LoginResult> { + const data: LoginTokenRequest = { + scope: "readwrite" as "write", //FIX: different than merchant + duration: { + // d_us: "forever" //FIX: should return shortest + d_us: 60 * 60 * 24 * 7 * 1000 * 1000 + }, + refreshable: true, + } + try { + const response = await request<LoginTokenSuccessResponse>(baseUrl, `accounts/${username}/token`, { + method: "POST", + basicAuth: { + username, + password, + }, + data, + contentType: "json" + }); + return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, expiration: response.data.expiration }; + } catch (error) { + if (error instanceof RequestError) { + return { valid: false, cause: error.cause }; + } + + return { + valid: false, cause: { + type: ErrorType.UNEXPECTED, + loading: false, + info: { + hasToken: true, + status: 0, + options: {}, + url: `/private/token`, + payload: {} + }, + exception: error, + message: (error instanceof Error ? error.message : "unpexepected error") + } + }; + } + }; + + async function refreshLoginToken( + baseUrl: string, + token: LoginToken + ): Promise<LoginResult> { + + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { + return { + valid: false, cause: { + type: ErrorType.CLIENT, + status: HttpStatusCode.Unauthorized, + message: "login token expired, login again.", + info: { + hasToken: true, + status: 401, + options: {}, + url: `/private/token`, + payload: {} + }, + payload: {} + }, + } + } + + return requestNewLoginToken(baseUrl, token.token) + } + return { requestNewLoginToken, refreshLoginToken } +} + +export interface LoginToken { + token: AccessToken, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +export type AccessToken = string & { + [__ac_token]: true; +}; + +type YesOrNo = "yes" | "no"; +export type LoginResult = { + valid: true; + token: AccessToken; + expiration: Timestamp; +} | { + valid: false; + cause: HttpError<{}>; +} + + +// DELETE /private/instances/$INSTANCE +export interface LoginTokenRequest { + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Can this token be refreshed? + // Defaults to false. + refreshable?: boolean; +} +export interface LoginTokenSuccessResponse { + // The login token that can be used to access resources + // that are in scope for some time. Must be prefixed + // with "Bearer " when used in the "Authorization" HTTP header. + // Will already begin with the RFC 8959 prefix. + access_token: AccessToken; + + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + expiration: Timestamp; + + // Can this token be refreshed? + refreshable: boolean; +} |