business account

This commit is contained in:
Sebastian 2023-02-10 09:51:37 -03:00
parent 53af8b486f
commit ba8b40c915
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
18 changed files with 670 additions and 400 deletions

View File

@ -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;

View File

@ -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,
};
}

View File

@ -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, {});

View File

@ -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>
);

View File

@ -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",

View File

@ -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;
}

View File

@ -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",
}
}
}

View File

@ -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 };
}

View File

@ -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);
};
}

View File

@ -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,
});
if (data) return data;
if (error) return error;
return { loading: true };
}
export function useAccountDetails(account: string): HttpResponse<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> {
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);
});
});
return isBusiness;
}
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)

View File

@ -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>
// );
// }

View File

@ -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 && (
@ -276,10 +280,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
@ -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();
<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);
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>

View File

@ -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">

View File

@ -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>
);
}

View File

@ -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} />;

View File

@ -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>
);

View File

@ -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;

View File

@ -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;