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"; import { LoadingUriView, ReadyView } from "./views.js";
export interface Props { export interface Props {
account: string; empty?: boolean;
} }
export type State = State.Loading | State.LoadingUriError | State.Ready; 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 { useCashouts } from "../../hooks/circuit.js";
import { Props, State, Transaction } from "./index.js"; import { Props, State, Transaction } from "./index.js";
export function useComponentState({ export function useComponentState({ empty }: Props): State {
account, const result = useCashouts();
}: Props): State {
const result = useCashouts()
if (result.loading) { if (result.loading) {
return { return {
status: "loading", status: "loading",
error: undefined error: undefined,
} };
} }
if (!result.ok) { if (!result.ok) {
return { return {
status: "loading-error", status: "loading-error",
error: result error: result,
} };
} }
return { return {
status: "ready", status: "ready",
error: undefined, error: undefined,
cashout: result.data, cashouts: result.data,
}; };
} }

View File

@ -26,20 +26,4 @@ export default {
title: "transaction list", title: "transaction list",
}; };
export const Ready = tests.createExample(ReadyView, { 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(),
},
},
],
});

View File

@ -39,7 +39,7 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
<tr> <tr>
<th>{i18n.str`Created`}</th> <th>{i18n.str`Created`}</th>
<th>{i18n.str`Confirmed`}</th> <th>{i18n.str`Confirmed`}</th>
<th>{i18n.str`Counterpart`}</th> <th>{i18n.str`Status`}</th>
<th>{i18n.str`Subject`}</th> <th>{i18n.str`Subject`}</th>
</tr> </tr>
</thead> </thead>
@ -53,8 +53,9 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss") ? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss")
: "-"} : "-"}
</td> </td>
<td>{Amounts.stringifyValue(item.amount_debit)}</td>
<td>{Amounts.stringifyValue(item.amount_credit)}</td> <td>{Amounts.stringifyValue(item.amount_credit)}</td>
<td>{item.counterpart}</td> <td>{item.status}</td>
<td>{item.subject}</td> <td>{item.subject}</td>
</tr> </tr>
); );

View File

@ -18,21 +18,19 @@ import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
import { useTransactions } from "../../hooks/access.js"; import { useTransactions } from "../../hooks/access.js";
import { Props, State, Transaction } from "./index.js"; import { Props, State, Transaction } from "./index.js";
export function useComponentState({ export function useComponentState({ account }: Props): State {
account, const result = useTransactions(account);
}: Props): State {
const result = useTransactions(account)
if (result.loading) { if (result.loading) {
return { return {
status: "loading", status: "loading",
error: undefined error: undefined,
} };
} }
if (!result.ok) { if (!result.ok) {
return { return {
status: "loading-error", status: "loading-error",
error: result error: result,
} };
} }
// if (error) { // if (error) {
// switch (error.status) { // switch (error.status) {
@ -73,53 +71,57 @@ export function useComponentState({
// }; // };
// } // }
const transactions = result.data.transactions.map((item: unknown) => { const transactions = result.data.transactions
if ( .map((item: unknown) => {
!item || if (
typeof item !== "object" || !item ||
!("direction" in item) || typeof item !== "object" ||
!("creditorIban" in item) || !("direction" in item) ||
!("debtorIban" in item) || !("creditorIban" in item) ||
!("date" in item) || !("debtorIban" in item) ||
!("subject" in item) || !("date" in item) ||
!("currency" in item) || !("subject" in item) ||
!("amount" in item) !("currency" in item) ||
) { !("amount" in item)
//not valid ) {
return; //not valid
} return;
const anyItem = item as any; }
if ( const anyItem = item as any;
!(typeof anyItem.creditorIban === "string") || if (
!(typeof anyItem.debtorIban === "string") || !(typeof anyItem.creditorIban === "string") ||
!(typeof anyItem.date === "string") || !(typeof anyItem.debtorIban === "string") ||
!(typeof anyItem.subject === "string") || !(typeof anyItem.date === "string") ||
!(typeof anyItem.currency === "string") || !(typeof anyItem.subject === "string") ||
!(typeof anyItem.amount === "string") !(typeof anyItem.currency === "string") ||
) { !(typeof anyItem.amount === "string")
return; ) {
} return;
}
const negative = anyItem.direction === "DBIT"; const negative = anyItem.direction === "DBIT";
const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban; const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
let date = anyItem.date ? parseInt(anyItem.date, 10) : 0 let date = anyItem.date ? parseInt(anyItem.date, 10) : 0;
if (isNaN(date) || !isFinite(date)) { if (isNaN(date) || !isFinite(date)) {
date = 0 date = 0;
} }
const when: AbsoluteTime = !date ? AbsoluteTime.never() : { const when: AbsoluteTime = !date
t_ms: date, ? AbsoluteTime.never()
}; : {
const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`); t_ms: date,
const subject = anyItem.subject; };
return { const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`);
negative, const subject = anyItem.subject;
counterpart, return {
when, negative,
amount, counterpart,
subject, when,
}; amount,
}).filter((x): x is Transaction => x !== undefined); subject,
};
})
.filter((x): x is Transaction => x !== undefined);
return { return {
status: "ready", status: "ready",

View File

@ -95,7 +95,7 @@ export type ErrorMessage = {
description?: string; description?: string;
title: TranslatedString; title: TranslatedString;
debug?: string; debug?: string;
} };
/** /**
* Track page state. * Track page state.
*/ */
@ -110,5 +110,4 @@ export interface PageStateType {
* be moved in a future "withdrawal state" object. * be moved in a future "withdrawal state" object.
*/ */
withdrawalId?: string; withdrawalId?: string;
} }

View File

@ -70,7 +70,6 @@ interface WireTransferRequestType {
amount?: string; amount?: string;
} }
type HashCode = string; type HashCode = string;
type EddsaPublicKey = string; type EddsaPublicKey = string;
type EddsaSignature = string; type EddsaSignature = string;
@ -101,7 +100,6 @@ type UUID = string;
type Integer = number; type Integer = number;
namespace SandboxBackend { namespace SandboxBackend {
export interface Config { export interface Config {
// Name of this API, always "circuit". // Name of this API, always "circuit".
name: string; name: string;
@ -126,7 +124,6 @@ namespace SandboxBackend {
error: SandboxErrorDetail; error: SandboxErrorDetail;
} }
interface SandboxErrorDetail { interface SandboxErrorDetail {
// String enum classifying the error. // String enum classifying the error.
type: ErrorType; type: ErrorType;
@ -147,13 +144,12 @@ namespace SandboxBackend {
* Sandbox and Nexus, therefore the actual meaning * Sandbox and Nexus, therefore the actual meaning
* must be carried by the error 'message' field. * must be carried by the error 'message' field.
*/ */
UtilError = "util-error" UtilError = "util-error",
} }
namespace Access { namespace Access {
interface PublicAccountsResponse { interface PublicAccountsResponse {
publicAccounts: PublicAccount[] publicAccounts: PublicAccount[];
} }
interface PublicAccount { interface PublicAccount {
iban: string; iban: string;
@ -213,7 +209,6 @@ namespace SandboxBackend {
} }
interface BankAccountTransactionInfo { interface BankAccountTransactionInfo {
creditorIban: string; creditorIban: string;
creditorBic: string; // Optional creditorBic: string; // Optional
creditorName: string; creditorName: string;
@ -233,7 +228,6 @@ namespace SandboxBackend {
date: string; // milliseconds since the Unix epoch date: string; // milliseconds since the Unix epoch
} }
interface CreateBankAccountTransactionCreate { interface CreateBankAccountTransactionCreate {
// Address in the Payto format of the wire transfer receiver. // Address in the Payto format of the wire transfer receiver.
// It needs at least the 'message' query string parameter. // It needs at least the 'message' query string parameter.
paytoUri: string; paytoUri: string;
@ -250,7 +244,6 @@ namespace SandboxBackend {
password: string; password: string;
} }
} }
namespace Circuit { namespace Circuit {
@ -281,7 +274,6 @@ namespace SandboxBackend {
internal_iban?: string; internal_iban?: string;
} }
interface CircuitContactData { interface CircuitContactData {
// E-Mail address // E-Mail address
email?: string; email?: string;
@ -289,7 +281,6 @@ namespace SandboxBackend {
phone?: string; phone?: string;
} }
interface CircuitAccountReconfiguration { interface CircuitAccountReconfiguration {
// Addresses where to send the TAN. // Addresses where to send the TAN.
contact_data: CircuitContactData; contact_data: CircuitContactData;
@ -300,7 +291,6 @@ namespace SandboxBackend {
cashout_address: string; cashout_address: string;
} }
interface AccountPasswordChange { interface AccountPasswordChange {
// New password. // New password.
new_password: string; new_password: string;
} }
@ -314,7 +304,6 @@ namespace SandboxBackend {
// Legal subject owning the account. // Legal subject owning the account.
name: string; name: string;
} }
interface CircuitAccountData { interface CircuitAccountData {
@ -336,10 +325,9 @@ namespace SandboxBackend {
enum TanChannel { enum TanChannel {
SMS = "sms", SMS = "sms",
EMAIL = "email", EMAIL = "email",
FILE = "file" FILE = "file",
} }
interface CashoutRequest { interface CashoutRequest {
// Optional subject to associate to the // Optional subject to associate to the
// cashout operation. This data will appear // cashout operation. This data will appear
// as the incoming wire transfer subject in // as the incoming wire transfer subject in
@ -370,7 +358,6 @@ namespace SandboxBackend {
uuid: string; uuid: string;
} }
interface CashoutConfirm { interface CashoutConfirm {
// the TAN that confirms $cashoutId. // the TAN that confirms $cashoutId.
tan: string; tan: string;
} }
@ -398,7 +385,6 @@ namespace SandboxBackend {
cashouts: string[]; cashouts: string[];
} }
interface CashoutStatusResponse { interface CashoutStatusResponse {
status: CashoutStatus; status: CashoutStatus;
// Amount debited to the circuit bank account. // Amount debited to the circuit bank account.
amount_debit: Amount; amount_debit: Amount;
@ -415,7 +401,6 @@ namespace SandboxBackend {
confirmation_time?: number | null; // milliseconds since the Unix epoch confirmation_time?: number | null; // milliseconds since the Unix epoch
} }
enum CashoutStatus { enum CashoutStatus {
// The payment was initiated after a valid // The payment was initiated after a valid
// TAN was received by the bank. // TAN was received by the bank.
CONFIRMED = "confirmed", CONFIRMED = "confirmed",
@ -425,5 +410,4 @@ namespace SandboxBackend {
PENDING = "pending", PENDING = "pending",
} }
} }
} }

View File

@ -14,91 +14,113 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 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 { import {
HttpError,
HttpResponse, HttpResponse,
HttpResponseOk, HttpResponseOk,
HttpResponsePaginated, HttpResponsePaginated,
RequestError,
} from "@gnu-taler/web-util/lib/index.browser"; } 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 { 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 { export function useAccessAPI(): AccessAPI {
const mutateAll = useMatchMutate(); const mutateAll = useMatchMutate();
const { request } = useAuthenticatedBackend(); const { request } = useAuthenticatedBackend();
const { state } = useBackendContext() const { state } = useBackendContext();
if (state.status === "loggedOut") { 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 ( const createWithdrawal = async (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
): Promise<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>> => { ): Promise<
const res = await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(`access-api/accounts/${account}/withdrawals`, { HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
method: "POST", > => {
data, const res =
contentType: "json" await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(
}); `access-api/accounts/${account}/withdrawals`,
{
method: "POST",
data,
contentType: "json",
},
);
return res; return res;
}; };
const abortWithdrawal = async ( const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => {
id: string, const res = await request<void>(
): Promise<HttpResponseOk<void>> => { `access-api/accounts/${account}/withdrawals/${id}`,
const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { {
method: "POST", method: "POST",
contentType: "json" contentType: "json",
}); },
);
await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
return res; return res;
}; };
const confirmWithdrawal = async ( const confirmWithdrawal = async (
id: string, id: string,
): Promise<HttpResponseOk<void>> => { ): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/accounts/${account}/withdrawals/${id}`, { const res = await request<void>(
method: "POST", `access-api/accounts/${account}/withdrawals/${id}`,
contentType: "json" {
}); method: "POST",
contentType: "json",
},
);
await mutateAll(/.*accounts\/.*\/withdrawals\/.*/); await mutateAll(/.*accounts\/.*\/withdrawals\/.*/);
return res; return res;
}; };
const createTransaction = async ( const createTransaction = async (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
): Promise<HttpResponseOk<void>> => { ): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/accounts/${account}/transactions`, { const res = await request<void>(
method: "POST", `access-api/accounts/${account}/transactions`,
data, {
contentType: "json" method: "POST",
}); data,
contentType: "json",
},
);
await mutateAll(/.*accounts\/.*\/transactions.*/); await mutateAll(/.*accounts\/.*\/transactions.*/);
return res; return res;
}; };
const deleteAccount = async ( const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/accounts/${account}`, { const res = await request<void>(`access-api/accounts/${account}`, {
method: "DELETE", method: "DELETE",
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*accounts\/.*/); await mutateAll(/.*accounts\/.*/);
return res; return res;
}; };
return { abortWithdrawal, confirmWithdrawal, createWithdrawal, createTransaction, deleteAccount }; return {
abortWithdrawal,
confirmWithdrawal,
createWithdrawal,
createTransaction,
deleteAccount,
};
} }
export function useTestingAPI(): TestingAPI { export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate(); const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend(); const { request: noAuthRequest } = usePublicBackend();
const register = async ( const register = async (
data: SandboxBackend.Access.BankRegistrationRequest data: SandboxBackend.Access.BankRegistrationRequest,
): Promise<HttpResponseOk<void>> => { ): Promise<HttpResponseOk<void>> => {
const res = await noAuthRequest<void>(`access-api/testing/register`, { const res = await noAuthRequest<void>(`access-api/testing/register`, {
method: "POST", method: "POST",
data, data,
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*accounts\/.*/); await mutateAll(/.*accounts\/.*/);
return res; return res;
@ -107,25 +129,22 @@ export function useTestingAPI(): TestingAPI {
return { register }; return { register };
} }
export interface TestingAPI { export interface TestingAPI {
register: ( register: (
data: SandboxBackend.Access.BankRegistrationRequest data: SandboxBackend.Access.BankRegistrationRequest,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
} }
export interface AccessAPI { export interface AccessAPI {
createWithdrawal: ( createWithdrawal: (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest, data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
) => Promise<HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>>; ) => Promise<
abortWithdrawal: ( HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
wid: string, >;
) => Promise<HttpResponseOk<void>>; abortWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
confirmWithdrawal: ( confirmWithdrawal: (wid: string) => Promise<HttpResponseOk<void>>;
wid: string
) => Promise<HttpResponseOk<void>>;
createTransaction: ( createTransaction: (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
deleteAccount: () => Promise<HttpResponseOk<void>>; deleteAccount: () => Promise<HttpResponseOk<void>>;
} }
@ -135,13 +154,17 @@ export interface InstanceTemplateFilter {
position?: string; position?: string;
} }
export function useAccountDetails(
export function useAccountDetails(account: string): HttpResponse<SandboxBackend.Access.BankAccountBalanceResponse, SandboxBackend.SandboxError> { account: string,
): HttpResponse<
SandboxBackend.Access.BankAccountBalanceResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend(); const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR< const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>, HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
HttpError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}`], fetcher, { >([`access-api/accounts/${account}`], fetcher, {
refreshInterval: 0, refreshInterval: 0,
refreshWhenHidden: false, refreshWhenHidden: false,
@ -155,17 +178,23 @@ export function useAccountDetails(account: string): HttpResponse<SandboxBackend.
}); });
if (data) return data; if (data) return data;
if (error) return error; if (error) return error.info;
return { loading: true }; return { loading: true };
} }
// FIXME: should poll // 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 { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR< const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>, HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
HttpError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, { >([`access-api/accounts/${account}/withdrawals/${wid}`], fetcher, {
refreshInterval: 1000, refreshInterval: 1000,
refreshWhenHidden: false, refreshWhenHidden: false,
@ -176,21 +205,26 @@ export function useWithdrawalDetails(account: string, wid: string): HttpResponse
errorRetryInterval: 1, errorRetryInterval: 1,
shouldRetryOnError: false, shouldRetryOnError: false,
keepPreviousData: true, keepPreviousData: true,
}); });
// if (isValidating) return { loading: true, data: data?.data }; // if (isValidating) return { loading: true, data: data?.data };
if (data) return data; if (data) return data;
if (error) return error; if (error) return error.info;
return { loading: true }; 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 { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR< const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>, HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
HttpError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}/transactions/${tid}`], fetcher, { >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
refreshInterval: 0, refreshInterval: 0,
refreshWhenHidden: false, refreshWhenHidden: false,
@ -205,17 +239,20 @@ export function useTransactionDetails(account: string, tid: string): HttpRespons
// if (isValidating) return { loading: true, data: data?.data }; // if (isValidating) return { loading: true, data: data?.data };
if (data) return data; if (data) return data;
if (error) return error; if (error) return error.info;
return { loading: true }; return { loading: true };
} }
interface PaginationFilter { interface PaginationFilter {
page: number, page: number;
} }
export function usePublicAccounts( export function usePublicAccounts(
args?: PaginationFilter, args?: PaginationFilter,
): HttpResponsePaginated<SandboxBackend.Access.PublicAccountsResponse, SandboxBackend.SandboxError> { ): HttpResponsePaginated<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend(); const { paginatedFetcher } = usePublicBackend();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -226,18 +263,21 @@ export function usePublicAccounts(
isValidating: loadingAfter, isValidating: loadingAfter,
} = useSWR< } = useSWR<
HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>, HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
HttpError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher); >([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState< const [lastAfter, setLastAfter] = useState<
HttpResponse<SandboxBackend.Access.PublicAccountsResponse, SandboxBackend.SandboxError> HttpResponse<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true }); >({ loading: true });
useEffect(() => { useEffect(() => {
if (afterData) setLastAfter(afterData); if (afterData) setLastAfter(afterData);
}, [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 // if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd = const isReachingEnd =
@ -254,30 +294,33 @@ export function usePublicAccounts(
} }
}, },
loadMorePrev: () => { loadMorePrev: () => {
null null;
}, },
}; };
const publicAccounts = !afterData ? [] : (afterData || lastAfter).data.publicAccounts; const publicAccounts = !afterData
if (loadingAfter) ? []
return { loading: true, data: { publicAccounts } }; : (afterData || lastAfter).data.publicAccounts;
if (loadingAfter) return { loading: true, data: { publicAccounts } };
if (afterData) { if (afterData) {
return { ok: true, data: { publicAccounts }, ...pagination }; return { ok: true, data: { publicAccounts }, ...pagination };
} }
return { loading: true }; return { loading: true };
} }
/** /**
* FIXME: mutate result when balance change (transaction ) * FIXME: mutate result when balance change (transaction )
* @param account * @param account
* @param args * @param args
* @returns * @returns
*/ */
export function useTransactions( export function useTransactions(
account: string, account: string,
args?: PaginationFilter, args?: PaginationFilter,
): HttpResponsePaginated<SandboxBackend.Access.BankAccountTransactionsResponse, SandboxBackend.SandboxError> { ): HttpResponsePaginated<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend(); const { paginatedFetcher } = useAuthenticatedBackend();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -288,18 +331,24 @@ export function useTransactions(
isValidating: loadingAfter, isValidating: loadingAfter,
} = useSWR< } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>, HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
HttpError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE], paginatedFetcher); >(
[`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
paginatedFetcher,
);
const [lastAfter, setLastAfter] = useState< const [lastAfter, setLastAfter] = useState<
HttpResponse<SandboxBackend.Access.BankAccountTransactionsResponse, SandboxBackend.SandboxError> HttpResponse<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true }); >({ loading: true });
useEffect(() => { useEffect(() => {
if (afterData) setLastAfter(afterData); if (afterData) setLastAfter(afterData);
}, [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 // if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd = const isReachingEnd =
@ -316,13 +365,14 @@ export function useTransactions(
} }
}, },
loadMorePrev: () => { loadMorePrev: () => {
null null;
}, },
}; };
const transactions = !afterData ? [] : (afterData || lastAfter).data.transactions; const transactions = !afterData
if (loadingAfter) ? []
return { loading: true, data: { transactions } }; : (afterData || lastAfter).data.transactions;
if (loadingAfter) return { loading: true, data: { transactions } };
if (afterData) { if (afterData) {
return { ok: true, data: { transactions }, ...pagination }; return { ok: true, data: { transactions }, ...pagination };
} }

View File

@ -15,7 +15,10 @@
*/ */
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; 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 { import {
HttpResponse, HttpResponse,
HttpResponseOk, HttpResponseOk,
@ -57,7 +60,7 @@ export function getInitialBackendBaseURL(): string {
export const defaultState: BackendState = { export const defaultState: BackendState = {
status: "loggedOut", status: "loggedOut",
url: getInitialBackendBaseURL() url: getInitialBackendBaseURL(),
}; };
export interface BackendStateHandler { export interface BackendStateHandler {
@ -91,7 +94,12 @@ export function useBackendState(): BackendStateHandler {
}, },
logIn(info) { logIn(info) {
//admin is defined by the username //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)); update(JSON.stringify(nextState));
}, },
}; };
@ -103,24 +111,25 @@ interface useBackendType {
options?: RequestOptions, options?: RequestOptions,
) => Promise<HttpResponseOk<T>>; ) => Promise<HttpResponseOk<T>>;
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
multiFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>[]>; multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
paginatedFetcher: <T>(args: [string, number, number]) => Promise<HttpResponseOk<T>>; paginatedFetcher: <T>(
sandboxAccountsFetcher: <T>(args: [string, number, number, string]) => Promise<HttpResponseOk<T>>; args: [string, number, number],
) => Promise<HttpResponseOk<T>>;
sandboxAccountsFetcher: <T>(
args: [string, number, number, string],
) => Promise<HttpResponseOk<T>>;
} }
export function usePublicBackend(): useBackendType { export function usePublicBackend(): useBackendType {
const { state } = useBackendContext(); const { state } = useBackendContext();
const { request: requestHandler } = useApiContext(); const { request: requestHandler } = useApiContext();
const baseUrl = state.url const baseUrl = state.url;
const request = useCallback( const request = useCallback(
function requestImpl<T>( function requestImpl<T>(
path: string, path: string,
options: RequestOptions = {}, options: RequestOptions = {},
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, path, options); return requestHandler<T>(baseUrl, path, options);
}, },
[baseUrl], [baseUrl],
@ -133,15 +142,21 @@ export function usePublicBackend(): useBackendType {
[baseUrl], [baseUrl],
); );
const paginatedFetcher = useCallback( const paginatedFetcher = useCallback(
function fetcherImpl<T>([endpoint, page, size]: [string, number, number]): Promise<HttpResponseOk<T>> { function fetcherImpl<T>([endpoint, page, size]: [
return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); string,
number,
number,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
params: { page: page || 1, size },
});
}, },
[baseUrl], [baseUrl],
); );
const multiFetcher = useCallback( const multiFetcher = useCallback(
function multiFetcherImpl<T>( function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
endpoints: string[], HttpResponseOk<T>[]
): Promise<HttpResponseOk<T>[]> { > {
return Promise.all( return Promise.all(
endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)), endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint)),
); );
@ -149,27 +164,39 @@ export function usePublicBackend(): useBackendType {
[baseUrl], [baseUrl],
); );
const sandboxAccountsFetcher = useCallback( const sandboxAccountsFetcher = useCallback(
function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { function fetcherImpl<T>([endpoint, page, size, account]: [
return requestHandler<T>(baseUrl, endpoint, { params: { page: page || 1, size } }); string,
number,
number,
string,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
params: { page: page || 1, size },
});
}, },
[baseUrl], [baseUrl],
); );
return { request, fetcher, paginatedFetcher, multiFetcher, sandboxAccountsFetcher }; return {
request,
fetcher,
paginatedFetcher,
multiFetcher,
sandboxAccountsFetcher,
};
} }
export function useAuthenticatedBackend(): useBackendType { export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext(); const { state } = useBackendContext();
const { request: requestHandler } = useApiContext(); const { request: requestHandler } = useApiContext();
const creds = state.status === "loggedIn" ? state : undefined const creds = state.status === "loggedIn" ? state : undefined;
const baseUrl = state.url const baseUrl = state.url;
const request = useCallback( const request = useCallback(
function requestImpl<T>( function requestImpl<T>(
path: string, path: string,
options: RequestOptions = {}, options: RequestOptions = {},
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options }); return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options });
}, },
[baseUrl, creds], [baseUrl, creds],
@ -182,36 +209,66 @@ export function useAuthenticatedBackend(): useBackendType {
[baseUrl, creds], [baseUrl, creds],
); );
const paginatedFetcher = useCallback( const paginatedFetcher = useCallback(
function fetcherImpl<T>([endpoint, page = 0, size]: [string, number, number]): Promise<HttpResponseOk<T>> { function fetcherImpl<T>([endpoint, page = 0, size]: [
return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page, size } }); string,
number,
number,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
basicAuth: creds,
params: { page, size },
});
}, },
[baseUrl, creds], [baseUrl, creds],
); );
const multiFetcher = useCallback( const multiFetcher = useCallback(
function multiFetcherImpl<T>( function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
endpoints: string[], HttpResponseOk<T>[]
): Promise<HttpResponseOk<T>[]> { > {
console.log("list size", endpoints.length, endpoints);
return Promise.all( return Promise.all(
endpoints.map((endpoint) => requestHandler<T>(baseUrl, endpoint, { basicAuth: creds })), endpoints.map((endpoint) =>
requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
),
); );
}, },
[baseUrl, creds], [baseUrl, creds],
); );
const sandboxAccountsFetcher = useCallback( const sandboxAccountsFetcher = useCallback(
function fetcherImpl<T>([endpoint, page, size, account]: [string, number, number, string]): Promise<HttpResponseOk<T>> { function fetcherImpl<T>([endpoint, page, size, account]: [
return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds, params: { page: page || 1, size } }); string,
number,
number,
string,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
basicAuth: creds,
params: { page: page || 1, size },
});
}, },
[baseUrl], [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(); const { request } = usePublicBackend();
type Type = SandboxBackend.Config; 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(() => { useEffect(() => {
request<Type>(`/config`) request<Type>(`/config`)
@ -238,10 +295,8 @@ export function useMatchMutate(): (
const allKeys = Array.from(cache.keys()); const allKeys = Array.from(cache.keys());
const keys = allKeys.filter((key) => re.test(key)); const keys = allKeys.filter((key) => re.test(key));
const mutations = keys.map((key) => { const mutations = keys.map((key) => {
mutate(key, value, true); return mutate(key, value, true);
}); });
return Promise.all(mutations); return Promise.all(mutations);
}; };
} }

View File

@ -15,23 +15,24 @@
*/ */
import { import {
HttpError,
HttpResponse, HttpResponse,
HttpResponseOk, HttpResponseOk,
HttpResponsePaginated, HttpResponsePaginated,
RequestError RequestError,
useApiContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { useEffect, useMemo, useState } from "preact/hooks"; import { useEffect, useMemo, useState } from "preact/hooks";
import useSWR from "swr"; import useSWR from "swr";
import { useBackendContext } from "../context/backend.js"; import { useBackendContext } from "../context/backend.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.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 { export function useAdminAccountAPI(): AdminAccountAPI {
const { request } = useAuthenticatedBackend(); const { request } = useAuthenticatedBackend();
const { state } = useBackendContext() const mutateAll = useMatchMutate();
const { state } = useBackendContext();
if (state.status === "loggedOut") { 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 ( const createAccount = async (
@ -40,8 +41,9 @@ export function useAdminAccountAPI(): AdminAccountAPI {
const res = await request<void>(`circuit-api/accounts`, { const res = await request<void>(`circuit-api/accounts`, {
method: "POST", method: "POST",
data, data,
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*circuit-api\/accounts.*/);
return res; return res;
}; };
@ -52,8 +54,9 @@ export function useAdminAccountAPI(): AdminAccountAPI {
const res = await request<void>(`circuit-api/accounts/${account}`, { const res = await request<void>(`circuit-api/accounts/${account}`, {
method: "PATCH", method: "PATCH",
data, data,
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*circuit-api\/accounts.*/);
return res; return res;
}; };
const deleteAccount = async ( const deleteAccount = async (
@ -61,8 +64,9 @@ export function useAdminAccountAPI(): AdminAccountAPI {
): Promise<HttpResponseOk<void>> => { ): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`circuit-api/accounts/${account}`, { const res = await request<void>(`circuit-api/accounts/${account}`, {
method: "DELETE", method: "DELETE",
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*circuit-api\/accounts.*/);
return res; return res;
}; };
const changePassword = async ( const changePassword = async (
@ -72,7 +76,7 @@ export function useAdminAccountAPI(): AdminAccountAPI {
const res = await request<void>(`circuit-api/accounts/${account}/auth`, { const res = await request<void>(`circuit-api/accounts/${account}/auth`, {
method: "PATCH", method: "PATCH",
data, data,
contentType: "json" contentType: "json",
}); });
return res; return res;
}; };
@ -82,9 +86,10 @@ export function useAdminAccountAPI(): AdminAccountAPI {
export function useCircuitAccountAPI(): CircuitAccountAPI { export function useCircuitAccountAPI(): CircuitAccountAPI {
const { request } = useAuthenticatedBackend(); const { request } = useAuthenticatedBackend();
const { state } = useBackendContext() const mutateAll = useMatchMutate();
const { state } = useBackendContext();
if (state.status === "loggedOut") { 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;
@ -94,8 +99,9 @@ export function useCircuitAccountAPI(): CircuitAccountAPI {
const res = await request<void>(`circuit-api/accounts/${account}`, { const res = await request<void>(`circuit-api/accounts/${account}`, {
method: "PATCH", method: "PATCH",
data, data,
contentType: "json" contentType: "json",
}); });
await mutateAll(/.*circuit-api\/accounts.*/);
return res; return res;
}; };
const changePassword = async ( const changePassword = async (
@ -104,7 +110,7 @@ export function useCircuitAccountAPI(): CircuitAccountAPI {
const res = await request<void>(`circuit-api/accounts/${account}/auth`, { const res = await request<void>(`circuit-api/accounts/${account}/auth`, {
method: "PATCH", method: "PATCH",
data, data,
contentType: "json" contentType: "json",
}); });
return res; return res;
}; };
@ -120,57 +126,72 @@ export interface AdminAccountAPI {
updateAccount: ( updateAccount: (
account: string, account: string,
data: SandboxBackend.Circuit.CircuitAccountReconfiguration data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
changePassword: ( changePassword: (
account: string, account: string,
data: SandboxBackend.Circuit.AccountPasswordChange data: SandboxBackend.Circuit.AccountPasswordChange,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
} }
export interface CircuitAccountAPI { export interface CircuitAccountAPI {
updateAccount: ( updateAccount: (
data: SandboxBackend.Circuit.CircuitAccountReconfiguration data: SandboxBackend.Circuit.CircuitAccountReconfiguration,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
changePassword: ( changePassword: (
data: SandboxBackend.Circuit.AccountPasswordChange data: SandboxBackend.Circuit.AccountPasswordChange,
) => Promise<HttpResponseOk<void>>; ) => Promise<HttpResponseOk<void>>;
} }
export interface InstanceTemplateFilter { export interface InstanceTemplateFilter {
//FIXME: add filter to the template list //FIXME: add filter to the template list
position?: string; position?: string;
} }
async function getBusinessStatus(
export function useMyAccountDetails(): HttpResponse<SandboxBackend.Circuit.CircuitAccountData, SandboxBackend.SandboxError> { request: ReturnType<typeof useApiContext>["request"],
const { fetcher } = useAuthenticatedBackend(); url: string,
const { state } = useBackendContext() basicAuth: { username: string; password: string },
if (state.status === "loggedOut") { ): Promise<boolean> {
throw Error("can't access my-account-details when logged out") 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 { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR< const { data, error } = useSWR<
@ -188,20 +209,22 @@ export function useAccountDetails(account: string): HttpResponse<SandboxBackend.
keepPreviousData: true, keepPreviousData: true,
}); });
// if (isValidating) return { loading: true, data: data?.data };
if (data) return data; if (data) return data;
if (error) return error.info; if (error) return error.info;
return { loading: true }; return { loading: true };
} }
interface PaginationFilter { interface PaginationFilter {
account?: string, account?: string;
page?: number, page?: number;
} }
export function useAccounts( export function useBusinessAccounts(
args?: PaginationFilter, args?: PaginationFilter,
): HttpResponsePaginated<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> { ): HttpResponsePaginated<
SandboxBackend.Circuit.CircuitAccounts,
SandboxBackend.SandboxError
> {
const { sandboxAccountsFetcher } = useAuthenticatedBackend(); const { sandboxAccountsFetcher } = useAuthenticatedBackend();
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@ -212,17 +235,21 @@ export function useAccounts(
} = useSWR< } = useSWR<
HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>, HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>,
RequestError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account], sandboxAccountsFetcher, { >(
refreshInterval: 0, [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account],
refreshWhenHidden: false, sandboxAccountsFetcher,
revalidateOnFocus: false, {
revalidateOnReconnect: false, refreshInterval: 0,
refreshWhenOffline: false, refreshWhenHidden: false,
errorRetryCount: 0, revalidateOnFocus: false,
errorRetryInterval: 1, revalidateOnReconnect: false,
shouldRetryOnError: false, refreshWhenOffline: false,
keepPreviousData: true, errorRetryCount: 0,
}); errorRetryInterval: 1,
shouldRetryOnError: false,
keepPreviousData: true,
},
);
// const [lastAfter, setLastAfter] = useState< // const [lastAfter, setLastAfter] = useState<
// HttpResponse<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError> // HttpResponse<SandboxBackend.Circuit.CircuitAccounts, SandboxBackend.SandboxError>
@ -247,18 +274,18 @@ export function useAccounts(
} }
}, },
loadMorePrev: () => { loadMorePrev: () => {
null null;
}, },
}; };
const result = useMemo(() => { const result = useMemo(() => {
const customers = !afterData ? [] : (afterData)?.data?.customers ?? []; const customers = !afterData ? [] : afterData?.data?.customers ?? [];
return { ok: true as const, data: { customers }, ...pagination } return { ok: true as const, data: { customers }, ...pagination };
}, [afterData?.data]) }, [afterData?.data]);
if (afterError) return afterError.info; if (afterError) return afterError.info;
if (afterData) { if (afterData) {
return result return result;
} }
// if (loadingAfter) // if (loadingAfter)

View File

@ -104,49 +104,48 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
)} )}
<section style={{ marginTop: "2em" }}> <section style={{ marginTop: "2em" }}>
<Moves account={account} /> <div class="active">
<h3>{i18n.str`Latest transactions`}</h3>
<Transactions account={account} />
</div>
</section> </section>
</Fragment> </Fragment>
); );
} }
function Moves({ account }: { account: string }): VNode { // function Moves({ account }: { account: string }): VNode {
const [tab, setTab] = useState<"transactions" | "cashouts">("transactions"); // const [tab, setTab] = useState<"transactions" | "cashouts">("transactions");
const { i18n } = useTranslationContext(); // const { i18n } = useTranslationContext();
return ( // return (
<article> // <article>
<div class="payments"> // <div class="payments">
<div class="tab"> // <div class="tab">
<button // <button
class={tab === "transactions" ? "tablinks active" : "tablinks"} // class={tab === "transactions" ? "tablinks active" : "tablinks"}
onClick={(): void => { // onClick={(): void => {
setTab("transactions"); // setTab("transactions");
}} // }}
> // >
{i18n.str`Transactions`} // {i18n.str`Transactions`}
</button> // </button>
<button // <button
class={tab === "cashouts" ? "tablinks active" : "tablinks"} // class={tab === "cashouts" ? "tablinks active" : "tablinks"}
onClick={(): void => { // onClick={(): void => {
setTab("cashouts"); // setTab("cashouts");
}} // }}
> // >
{i18n.str`Cashouts`} // {i18n.str`Cashouts`}
</button> // </button>
</div> // </div>
{tab === "transactions" && ( // {tab === "transactions" && (
<div class="active"> // )}
<h3>{i18n.str`Latest transactions`}</h3> // {tab === "cashouts" && (
<Transactions account={account} /> // <div class="active">
</div> // <h3>{i18n.str`Latest cashouts`}</h3>
)} // <Cashouts account={account} />
{tab === "cashouts" && ( // </div>
<div class="active"> // )}
<h3>{i18n.str`Latest cashouts`}</h3> // </div>
<Cashouts account={account} /> // </article>
</div> // );
)} // }
</div>
</article>
);
}

View File

@ -24,8 +24,8 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { ErrorMessage, usePageContext } from "../context/pageState.js"; import { ErrorMessage, usePageContext } from "../context/pageState.js";
import { import {
useAccountDetails, useBusinessAccountDetails,
useAccounts, useBusinessAccounts,
useAdminAccountAPI, useAdminAccountAPI,
} from "../hooks/circuit.js"; } from "../hooks/circuit.js";
import { import {
@ -71,7 +71,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
})); }));
} }
const result = useAccounts({ account }); const result = useBusinessAccounts({ account });
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
if (result.loading) return <div />; if (result.loading) return <div />;
@ -86,6 +86,10 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
<ShowAccountDetails <ShowAccountDetails
account={showDetails} account={showDetails}
onLoadNotOk={onLoadNotOk} onLoadNotOk={onLoadNotOk}
onChangePassword={() => {
setUpdatePassword(showDetails);
setShowDetails(undefined);
}}
onUpdateSuccess={() => { onUpdateSuccess={() => {
showInfoMessage(i18n.str`Account updated`); showInfoMessage(i18n.str`Account updated`);
setShowDetails(undefined); setShowDetails(undefined);
@ -230,7 +234,7 @@ function initializeFromTemplate(
return initial as any; return initial as any;
} }
function UpdateAccountPassword({ export function UpdateAccountPassword({
account, account,
onClear, onClear,
onUpdateSuccess, onUpdateSuccess,
@ -242,7 +246,7 @@ function UpdateAccountPassword({
account: string; account: string;
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const result = useAccountDetails(account); const result = useBusinessAccountDetails(account);
const { changePassword } = useAdminAccountAPI(); const { changePassword } = useAdminAccountAPI();
const [password, setPassword] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>();
@ -268,7 +272,7 @@ function UpdateAccountPassword({
<div> <div>
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate>Admin panel</i18n.Translate> <i18n.Translate>Update password for {account}</i18n.Translate>
</h1> </h1>
</div> </div>
{error && ( {error && (
@ -276,10 +280,6 @@ function UpdateAccountPassword({
)} )}
<form class="pure-form"> <form class="pure-form">
<fieldset>
<label for="username">{i18n.str`Username`}</label>
<input name="username" type="text" readOnly value={account} />
</fieldset>
<fieldset> <fieldset>
<label>{i18n.str`Password`}</label> <label>{i18n.str`Password`}</label>
<input <input
@ -366,7 +366,7 @@ function CreateNewAccount({
<div> <div>
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate>Admin panel</i18n.Translate> <i18n.Translate>New account</i18n.Translate>
</h1> </h1>
</div> </div>
{error && ( {error && (
@ -428,19 +428,21 @@ function CreateNewAccount({
); );
} }
function ShowAccountDetails({ export function ShowAccountDetails({
account, account,
onClear, onClear,
onUpdateSuccess, onUpdateSuccess,
onLoadNotOk, onLoadNotOk,
onChangePassword,
}: { }: {
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
onClear: () => void; onClear?: () => void;
onChangePassword: () => void;
onUpdateSuccess: () => void; onUpdateSuccess: () => void;
account: string; account: string;
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const result = useAccountDetails(account); const result = useBusinessAccountDetails(account);
const { updateAccount } = useAdminAccountAPI(); const { updateAccount } = useAdminAccountAPI();
const [update, setUpdate] = useState(false); const [update, setUpdate] = useState(false);
const [submitAccount, setSubmitAccount] = useState< const [submitAccount, setSubmitAccount] = useState<
@ -459,7 +461,7 @@ function ShowAccountDetails({
<div> <div>
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate>Admin panel</i18n.Translate> <i18n.Translate>Business account details</i18n.Translate>
</h1> </h1>
</div> </div>
{error && ( {error && (
@ -474,42 +476,59 @@ function ShowAccountDetails({
<p> <p>
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<div> <div>
<input {onClear ? (
class="pure-button" <input
type="submit" class="pure-button"
value={i18n.str`Close`} type="submit"
onClick={async (e) => { value={i18n.str`Close`}
e.preventDefault(); onClick={async (e) => {
onClear(); e.preventDefault();
}} onClear();
/> }}
/>
) : undefined}
</div> </div>
<div> <div style={{ display: "flex" }}>
<input <div>
id="select-exchange" <input
class="pure-button pure-button-primary content" id="select-exchange"
disabled={update && !submitAccount} class="pure-button pure-button-primary content"
type="submit" disabled={update && !submitAccount}
value={update ? i18n.str`Confirm` : i18n.str`Update`} type="submit"
onClick={async (e) => { value={i18n.str`Change password`}
e.preventDefault(); 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) { if (!update) {
setUpdate(true); setUpdate(true);
} else { } else {
if (!submitAccount) return; if (!submitAccount) return;
try { try {
await updateAccount(account, { await updateAccount(account, {
cashout_address: submitAccount.cashout_address, cashout_address: submitAccount.cashout_address,
contact_data: submitAccount.contact_data, contact_data: submitAccount.contact_data,
}); });
onUpdateSuccess(); onUpdateSuccess();
} catch (error) { } catch (error) {
handleError(error, saveError, i18n); handleError(error, saveError, i18n);
}
} }
} }}
}} />
/> </div>
</div> </div>
</div> </div>
</p> </p>

View File

@ -15,6 +15,7 @@
*/ */
import { Logger } from "@gnu-taler/taler-util"; 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 { ComponentChildren, Fragment, h, VNode } from "preact";
import talerLogo from "../assets/logo-white.svg"; import talerLogo from "../assets/logo-white.svg";
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js"; import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
@ -24,41 +25,46 @@ import {
PageStateType, PageStateType,
usePageContext, usePageContext,
} from "../context/pageState.js"; } 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"; import { bankUiSettings } from "../settings.js";
const logger = new Logger("BankFrame"); 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({ export function BankFrame({
children, children,
goToBusinessAccount,
}: { }: {
children: ComponentChildren; children: ComponentChildren;
goToBusinessAccount?: () => void;
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const backend = useBackendContext(); const backend = useBackendContext();
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
logger.trace("state", pageState); 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 = []; const demo_sites = [];
for (const i in bankUiSettings.demoSites) for (const i in bankUiSettings.demoSites)
@ -120,7 +126,36 @@ export function BankFrame({
/> />
)} )}
<StatusBanner /> <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} {children}
</section> </section>
<section id="footer" class="footer"> <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 { function saveErrorAndLogout(error: PageStateType["error"]): void {
console.log("rrot", error);
saveError(error); saveError(error);
backend.logOut(); backend.logOut();
} }
@ -123,6 +124,7 @@ function handleNotOkResult(
return function handleNotOkResult2<T, E>( return function handleNotOkResult2<T, E>(
result: HttpResponsePaginated<T, E>, result: HttpResponsePaginated<T, E>,
): VNode { ): VNode {
console.log("qweqwe", JSON.stringify(result, undefined, 2));
if (result.clientError && result.isUnauthorized) { if (result.clientError && result.isUnauthorized) {
onErrorHandler({ onErrorHandler({
title: i18n.str`Wrong credentials for "${account}"`, title: i18n.str`Wrong credentials for "${account}"`,
@ -139,7 +141,7 @@ function handleNotOkResult(
if (!result.ok) { if (!result.ok) {
onErrorHandler({ onErrorHandler({
title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, 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), debug: JSON.stringify(result.error),
}); });
return <LoginForm onRegister={onRegister} />; return <LoginForm onRegister={onRegister} />;

View File

@ -28,6 +28,7 @@ import { HomePage } from "./HomePage.js";
import { BankFrame } from "./BankFrame.js"; import { BankFrame } from "./BankFrame.js";
import { PublicHistoriesPage } from "./PublicHistoriesPage.js"; import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
import { RegistrationPage } from "./RegistrationPage.js"; import { RegistrationPage } from "./RegistrationPage.js";
import { BusinessAccount } from "./BusinessAccount.js";
function handleNotOkResult( function handleNotOkResult(
safe: string, safe: string,
@ -96,7 +97,11 @@ export function Routing(): VNode {
<Route <Route
path="/account" path="/account"
component={() => ( component={() => (
<BankFrame> <BankFrame
goToBusinessAccount={() => {
route("/business");
}}
>
<HomePage <HomePage
onRegister={() => { onRegister={() => {
route("/register"); route("/register");
@ -105,6 +110,22 @@ export function Routing(): VNode {
</BankFrame> </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" /> <Route default component={Redirect} to="/account" />
</Router> </Router>
); );

View File

@ -51,8 +51,11 @@ input[type="number"]::-webkit-inner-spin-button {
overflow: hidden; overflow: hidden;
} }
.logout { .top-right {
float: right; float: right;
}
.some-space {
display: inline-block;
border: 20px; border: 20px;
margin-right: 15px; margin-right: 15px;
margin-top: 15px; margin-top: 15px;

View File

@ -54,8 +54,10 @@ export type PartialButDefined<T> = {
}; };
export type WithIntermediate<Type extends object> = { 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> { // export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> {
// const root = obj === undefined ? {} : obj; // const root = obj === undefined ? {} : obj;