aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/hooks
diff options
context:
space:
mode:
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
commitfe7b51ef2736edbf04f5bbd9d19f2a2d04baccc2 (patch)
tree66c68c8d6a666f6e74dc663c9ee4f07879f6626c /packages/demobank-ui/src/hooks
parent35611f0bf9cf67638b171c2a300fab1797d3d8f0 (diff)
parent97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff)
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages/demobank-ui/src/hooks')
-rw-r--r--packages/demobank-ui/src/hooks/access.ts131
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts113
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts29
-rw-r--r--packages/demobank-ui/src/hooks/config.ts59
-rw-r--r--packages/demobank-ui/src/hooks/notification.ts54
-rw-r--r--packages/demobank-ui/src/hooks/settings.ts22
-rw-r--r--packages/demobank-ui/src/hooks/useCredentialsChecker.ts135
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;
+}