towards new core bank api

This commit is contained in:
Sebastian 2023-09-22 18:34:49 -03:00
parent 5640f0a67d
commit 15af6c619d
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
19 changed files with 3293 additions and 3158 deletions

View File

@ -27,6 +27,7 @@ import { Test } from "../pages/Test.js";
import { useBackendContext } from "../context/backend.js";
import { LoginForm } from "../pages/LoginForm.js";
import { AdminHome } from "../pages/admin/Home.js";
import { bankUiSettings } from "../settings.js";
export function Routing(): VNode {
const history = createHashHistory();
@ -45,6 +46,10 @@ export function Routing(): VNode {
/>
)}
/>
<Route
path="/public-accounts"
component={() => <PublicHistoriesPage />}
/>
<Route
path="/operation/:wopid"
component={({ wopid }: { wopid: string }) => (
@ -53,22 +58,21 @@ export function Routing(): VNode {
onContinue={() => {
route("/account");
}}
// onLoadNotOk={() => {
// route("/account");
// }}
/>
)}
/>
<Route
path="/register"
component={() => (
<RegistrationPage
onComplete={() => {
route("/account");
}}
/>
)}
/>
{bankUiSettings.allowRegistrations &&
<Route
path="/register"
component={() => (
<RegistrationPage
onComplete={() => {
route("/account");
}}
/>
)}
/>
}
<Route default component={Redirect} to="/login" />
</Router>
</BankFrame>
@ -93,16 +97,6 @@ export function Routing(): VNode {
path="/public-accounts"
component={() => <PublicHistoriesPage />}
/>
<Route
path="/register"
component={() => (
<RegistrationPage
onComplete={() => {
route("/account");
}}
/>
)}
/>
<Route
path="/account"
component={() => {

View File

@ -99,11 +99,6 @@ type Amount = string;
type UUID = string;
type Integer = number;
interface Balance {
amount: Amount;
credit_debit_indicator: "credit" | "debit";
}
namespace SandboxBackend {
export interface Config {
// Name of this API, always "circuit".
@ -126,7 +121,7 @@ namespace SandboxBackend {
}
export interface SandboxError {
error: SandboxErrorDetail;
error?: SandboxErrorDetail;
}
interface SandboxErrorDetail {
// String enum classifying the error.
@ -152,26 +147,12 @@ namespace SandboxBackend {
UtilError = "util-error",
}
namespace Access {
interface PublicAccountsResponse {
publicAccounts: PublicAccount[];
}
interface PublicAccount {
iban: string;
balance: string;
// The account name _and_ the username of the
// Sandbox customer that owns such a bank account.
accountLabel: string;
}
interface BankAccountBalanceResponse {
// Available balance on the account.
balance: Balance;
// payto://-URI of the account. (New)
paytoUri: string;
// Number indicating the max debit allowed for the requesting user.
debitThreshold: Amount;
}
type EmailAddress = string;
type PhoneNumber = string;
namespace CoreBank {
interface BankAccountCreateWithdrawalRequest {
// Amount to withdraw.
amount: Amount;
@ -243,11 +224,144 @@ namespace SandboxBackend {
amount?: string;
}
interface BankRegistrationRequest {
interface RegisterAccountRequest {
// Username
username: string;
// Password.
password: string;
// Legal name of the account owner
name: string;
// Defaults to false.
is_public?: boolean;
// Is this a taler exchange account?
// If true:
// - incoming transactions to the account that do not
// have a valid reserve public key are automatically
// - the account provides the taler-wire-gateway-api endpoints
// Defaults to false.
is_taler_exchange?: boolean;
// Addresses where to send the TAN for transactions.
// Currently only used for cashouts.
// If missing, cashouts will fail.
// In the future, might be used for other transactions
// as well.
challenge_contact_data?: ChallengeContactData;
// 'payto' address pointing a bank account
// external to the libeufin-bank.
// Payments will be sent to this bank account
// when the user wants to convert the local currency
// back to fiat currency outside libeufin-bank.
cashout_payto_uri?: string;
// Internal payto URI of this bank account.
// Used mostly for testing.
internal_payto_uri?: string;
}
interface ChallengeContactData {
// E-Mail address
email?: EmailAddress;
// Phone number.
phone?: PhoneNumber;
}
interface AccountReconfiguration {
// Addresses where to send the TAN for transactions.
// Currently only used for cashouts.
// If missing, cashouts will fail.
// In the future, might be used for other transactions
// as well.
challenge_contact_data?: ChallengeContactData;
// 'payto' address pointing a bank account
// external to the libeufin-bank.
// Payments will be sent to this bank account
// when the user wants to convert the local currency
// back to fiat currency outside libeufin-bank.
cashout_address?: string;
// Legal name associated with $username.
// When missing, the old name is kept.
name?: string;
// If present, change the is_exchange configuration.
// See RegisterAccountRequest
is_exchange?: boolean;
}
interface AccountPasswordChange {
// New password.
new_password: string;
}
interface PublicAccountsResponse {
public_accounts: PublicAccount[];
}
interface PublicAccount {
payto_uri: string;
balance: Balance;
// The account name (=username) of the
// libeufin-bank account.
account_name: string;
}
interface ListBankAccountsResponse {
accounts: AccountMinimalData[];
}
// interface Balance {
// amount: Amount;
// credit_debit_indicator: "credit" | "debit";
// }
type Balance = Amount
interface AccountMinimalData {
// Username
username: string;
// Legal name of the account owner.
name: string;
// current balance of the account
balance: Balance;
// Number indicating the max debit allowed for the requesting user.
debit_threshold: Amount;
}
interface AccountData {
// Legal name of the account owner.
name: string;
// Available balance on the account.
balance: Balance;
// payto://-URI of the account.
payto_uri: string;
// Number indicating the max debit allowed for the requesting user.
debit_threshold: Amount;
contact_data?: ChallengeContactData;
// 'payto' address pointing the bank account
// where to send cashouts. This field is optional
// because not all the accounts are required to participate
// in the merchants' circuit. One example is the exchange:
// that never cashouts. Registering these accounts can
// be done via the access API.
cashout_payto_uri?: string;
}
}
namespace Circuit {

View File

@ -44,13 +44,13 @@ export function useAccessAPI(): AccessAPI {
const account = state.username;
const createWithdrawal = async (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
): Promise<
HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
> => {
const res =
await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(
`access-api/accounts/${account}/withdrawals`,
await request<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>(
`accounts/${account}/withdrawals`,
{
method: "POST",
data,
@ -60,10 +60,10 @@ export function useAccessAPI(): AccessAPI {
return res;
};
const createTransaction = async (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(
`access-api/accounts/${account}/transactions`,
`accounts/${account}/transactions`,
{
method: "POST",
data,
@ -74,7 +74,7 @@ export function useAccessAPI(): AccessAPI {
return res;
};
const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/accounts/${account}`, {
const res = await request<void>(`accounts/${account}`, {
method: "DELETE",
contentType: "json",
});
@ -94,7 +94,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const { request } = useAuthenticatedBackend();
const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/withdrawals/${id}/abort`, {
const res = await request<void>(`accounts/withdrawals/${id}/abort`, {
method: "POST",
contentType: "json",
});
@ -104,7 +104,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const confirmWithdrawal = async (
id: string,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`access-api/withdrawals/${id}/confirm`, {
const res = await request<void>(`withdrawals/${id}/confirm`, {
method: "POST",
contentType: "json",
});
@ -122,10 +122,10 @@ export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend();
const register = async (
data: SandboxBackend.Access.BankRegistrationRequest,
data: SandboxBackend.CoreBank.RegisterAccountRequest,
): Promise<HttpResponseOk<void>> => {
// FIXME: This API is deprecated. The normal account registration API should be used instead.
const res = await noAuthRequest<void>(`access-api/testing/register`, {
const res = await noAuthRequest<void>(`accounts`, {
method: "POST",
data,
contentType: "json",
@ -139,18 +139,18 @@ export function useTestingAPI(): TestingAPI {
export interface TestingAPI {
register: (
data: SandboxBackend.Access.BankRegistrationRequest,
data: SandboxBackend.CoreBank.RegisterAccountRequest,
) => Promise<HttpResponseOk<void>>;
}
export interface AccessAPI {
createWithdrawal: (
data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
) => Promise<
HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
>;
createTransaction: (
data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
) => Promise<HttpResponseOk<void>>;
deleteAccount: () => Promise<HttpResponseOk<void>>;
}
@ -167,15 +167,15 @@ export interface InstanceTemplateFilter {
export function useAccountDetails(
account: string,
): HttpResponse<
SandboxBackend.Access.BankAccountBalanceResponse,
SandboxBackend.CoreBank.AccountData,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
HttpResponseOk<SandboxBackend.CoreBank.AccountData>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}`], fetcher, {
>([`accounts/${account}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -187,28 +187,8 @@ export function useAccountDetails(
keepPreviousData: true,
});
//FIXME: remove optional when libeufin sandbox has implemented the feature
if (data && typeof data.data.debitThreshold === "undefined") {
data.data.debitThreshold = "0";
}
//FIXME: sandbox server should return amount string
if (data) {
const isAmount = Amounts.parse(data.data.debitThreshold);
if (isAmount) {
//server response with correct format
return data;
}
const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
const clone = structuredClone(data);
const theNumber = Number.parseInt(data.data.debitThreshold, 10);
const value = Number.isNaN(theNumber) ? 0 : theNumber;
clone.data.debitThreshold = Amounts.stringify({
currency,
value: value,
fraction: 0,
});
return clone;
return data;
}
if (error) return error.cause;
return { loading: true };
@ -218,15 +198,15 @@ export function useAccountDetails(
export function useWithdrawalDetails(
wid: string,
): HttpResponse<
SandboxBackend.Access.BankAccountGetWithdrawalResponse,
SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
HttpResponseOk<SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/withdrawals/${wid}`], fetcher, {
>([`withdrawals/${wid}`], fetcher, {
refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -248,15 +228,15 @@ export function useTransactionDetails(
account: string,
tid: string,
): HttpResponse<
SandboxBackend.Access.BankAccountTransactionInfo,
SandboxBackend.CoreBank.BankAccountTransactionInfo,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { paginatedFetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionInfo>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
>([`accounts/${account}/transactions/${tid}`], paginatedFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -281,7 +261,7 @@ interface PaginationFilter {
export function usePublicAccounts(
args?: PaginationFilter,
): HttpResponsePaginated<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend();
@ -293,13 +273,13 @@ export function usePublicAccounts(
error: afterError,
isValidating: loadingAfter,
} = useSWR<
HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>,
RequestError<SandboxBackend.SandboxError>
>([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
>([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
SandboxBackend.Access.PublicAccountsResponse,
SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@ -312,7 +292,7 @@ export function usePublicAccounts(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
afterData && afterData.data.public_accounts.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
@ -320,7 +300,7 @@ export function usePublicAccounts(
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
@ -329,12 +309,12 @@ export function usePublicAccounts(
},
};
const publicAccounts = !afterData
const public_accounts = !afterData
? []
: (afterData || lastAfter).data.publicAccounts;
if (loadingAfter) return { loading: true, data: { publicAccounts } };
: (afterData || lastAfter).data.public_accounts;
if (loadingAfter) return { loading: true, data: { public_accounts } };
if (afterData) {
return { ok: true, data: { publicAccounts }, ...pagination };
return { ok: true, data: { public_accounts }, ...pagination };
}
return { loading: true };
}
@ -349,7 +329,7 @@ export function useTransactions(
account: string,
args?: PaginationFilter,
): HttpResponsePaginated<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend();
@ -361,10 +341,10 @@ export function useTransactions(
error: afterError,
isValidating: loadingAfter,
} = useSWR<
HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
RequestError<SandboxBackend.SandboxError>
>(
[`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
[`accounts/${account}/transactions`, args?.page, PAGE_SIZE],
paginatedFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
@ -378,7 +358,7 @@ export function useTransactions(
const [lastAfter, setLastAfter] = useState<
HttpResponse<
SandboxBackend.Access.BankAccountTransactionsResponse,
SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });

View File

@ -310,7 +310,7 @@ export function useAuthenticatedBackend(): useBackendType {
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
basicAuth: creds,
params: { page, size },
params: { delta: size, start: size * page },
});
},
[baseUrl, creds],

View File

@ -9,20 +9,25 @@ export function useCredentialsChecker() {
//while merchant backend doesn't have a login endpoint
async function requestNewLoginToken(
username: string,
password: AccessToken,
password: string,
): Promise<LoginResult> {
const data: LoginTokenRequest = {
scope: "write",
scope: "readwrite" as "write", //FIX: different than merchant
duration: {
d_us: "forever"
// d_us: "forever" //FIX: should return shortest
d_us: 1000 * 60 * 60 * 23
},
refreshable: true,
}
try {
const response = await request<LoginTokenSuccessResponse>(baseUrl, `accounts/${username}/token`, {
method: "POST",
token: password,
data
basicAuth: {
username: username,
password,
},
data,
contentType: "json"
});
return { valid: true, token: response.data.token, expiration: response.data.expiration };
} catch (error) {

View File

@ -60,7 +60,7 @@ export namespace State {
export interface InvalidIban {
status: "invalid-iban",
error: HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>;
error: HttpResponseOk<SandboxBackend.CoreBank.AccountData>;
}
export interface UserNotFound {

View File

@ -40,7 +40,7 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe
};
}
//logout if there is any error, not if loading
backend.logOut();
// backend.logOut();
if (result.status === HttpStatusCode.NotFound) {
notifyError(i18n.str`Username or account label "${account}" not found`, undefined);
return {
@ -55,9 +55,13 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe
}
const { data } = result;
const balance = Amounts.parseOrThrow(data.balance.amount);
const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
const payto = parsePaytoUri(data.paytoUri);
// FIXME: balance
// const balance = Amounts.parseOrThrow(data.balance.amount);
const balance = Amounts.parseOrThrow(data.balance);
const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
const payto = parsePaytoUri(data.payto_uri);
if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
return {
@ -66,7 +70,9 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe
};
}
const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
// FIXME: balance
const balanceIsDebit = true;
// data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;

View File

@ -27,7 +27,7 @@ import { useSettings } from "../../hooks/settings.js";
export function InvalidIbanView({ error }: State.InvalidIban) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
<div>Payto from server is not valid &quot;{error.data.payto_uri}&quot;</div>
);
}

View File

@ -458,15 +458,14 @@ function WelcomeAccount({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
if (!result.ok) return <div />
// const account = "Sebastian"
const payto = parsePaytoUri(result.data.paytoUri)
const payto = parsePaytoUri(result.data.payto_uri)
if (!payto) return <div />
const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
return <i18n.Translate>
Welcome, {account} {accountNumber !== undefined ?
<span>
(<a href={result.data.paytoUri}>{accountNumber}</a> <CopyButton getContent={() => result.data.paytoUri} />)
(<a href={result.data.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.data.payto_uri} />)
</span>
: <Fragment />}!
</i18n.Translate>
@ -477,9 +476,12 @@ function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
if (!result.ok) return <div />
// FIXME: balance
return <div>
{Amounts.currencyOf(result.data.balance.amount)}
{Amounts.currencyOf(result.data.balance)}
{Amounts.stringifyValue(result.data.balance)}
{/* {Amounts.currencyOf(result.data.balance.amount)}
&nbsp;{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""}
{Amounts.stringifyValue(result.data.balance.amount)}
{Amounts.stringifyValue(result.data.balance.amount)} */}
</div>
}

View File

@ -120,8 +120,8 @@ export function handleNotOkResult(
) => VNode {
return function handleNotOkResult2<T>(
result:
| HttpResponsePaginated<T, SandboxBackend.SandboxError>
| HttpResponse<T, SandboxBackend.SandboxError>,
| HttpResponsePaginated<T, SandboxBackend.SandboxError | undefined>
| HttpResponse<T, SandboxBackend.SandboxError| undefined>,
): VNode {
if (result.loading) return <Loading />;
if (!result.ok) {
@ -139,7 +139,7 @@ export function handleNotOkResult(
notify({
type: "error",
title: i18n.str`Could not load due to a client error`,
description: errorData.error.description as TranslatedString,
description: errorData?.error?.description as TranslatedString,
debug: JSON.stringify(result),
});
break;
@ -148,7 +148,7 @@ export function handleNotOkResult(
notify({
type: "error",
title: i18n.str`Server returned with error`,
description: result.payload.error.description as TranslatedString,
description: result.payload?.error?.description as TranslatedString,
debug: JSON.stringify(result.payload),
});
break;

View File

@ -31,12 +31,13 @@ import { useCredentialsCheckerOld } from "../hooks/backend.js";
*/
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const backend = useBackendContext();
const [username, setUsername] = useState<string | undefined>();
const currentUser = backend.state.status === "loggedIn" ? backend.state.username : undefined
const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
// const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
const testLogin = useCredentialsCheckerOld();
// const testLogin = useCredentialsCheckerOld();
const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() {
ref.current?.focus();
@ -46,8 +47,8 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const errors = undefinedIfEmpty({
username: !username
? i18n.str`Missing username`
: !USERNAME_REGEX.test(username)
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
// : !USERNAME_REGEX.test(username)
// ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
password: !password ? i18n.str`Missing password` : undefined,
}) ?? busy;
@ -59,19 +60,18 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
async function doLogin() {
if (!username || !password) return;
setBusy({})
const testResult = await testLogin(username, password);
if (testResult.valid) {
const result = await requestNewLoginToken(username, password);
if (result.valid) {
backend.logIn({ username, password });
} else {
if (testResult.requestError) {
const { cause } = testResult;
switch (cause.type) {
case ErrorType.CLIENT: {
if (cause.status === HttpStatusCode.Unauthorized) {
saveError({
title: i18n.str`Wrong credentials for "${username}"`,
});
} else
const { cause } = result;
switch (cause.type) {
case ErrorType.CLIENT: {
if (cause.status === HttpStatusCode.Unauthorized) {
saveError({
title: i18n.str`Wrong credentials for "${username}"`,
});
} else
if (cause.status === HttpStatusCode.NotFound) {
saveError({
title: i18n.str`Account not found`,
@ -79,50 +79,44 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
} else {
saveError({
title: i18n.str`Could not load due to a client error`,
description: cause.payload.error.description,
// description: cause.payload.error.description,
debug: JSON.stringify(cause.payload),
});
}
break;
}
case ErrorType.SERVER: {
saveError({
title: i18n.str`Server had a problem, try again later or report.`,
description: cause.payload.error.description,
debug: JSON.stringify(cause.payload),
});
break;
}
case ErrorType.TIMEOUT: {
saveError({
title: i18n.str`Request timeout, try again later.`,
});
break;
}
case ErrorType.UNREADABLE: {
saveError({
title: i18n.str`Unexpected error.`,
description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
debug: JSON.stringify(cause),
});
break;
}
default: {
saveError({
title: i18n.str`Unexpected error, please report.`,
description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
debug: JSON.stringify(cause),
});
break;
}
break;
}
case ErrorType.SERVER: {
saveError({
title: i18n.str`Server had a problem, try again later or report.`,
// description: cause.payload.error.description,
debug: JSON.stringify(cause.payload),
});
break;
}
case ErrorType.TIMEOUT: {
saveError({
title: i18n.str`Request timeout, try again later.`,
});
break;
}
case ErrorType.UNREADABLE: {
saveError({
title: i18n.str`Unexpected error.`,
description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
debug: JSON.stringify(cause),
});
break;
}
default: {
saveError({
title: i18n.str`Unexpected error, please report.`,
description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
debug: JSON.stringify(cause),
});
break;
}
} else {
saveError({
title: i18n.str`Unexpected error, please report.`,
debug: JSON.stringify(testResult.error),
});
}
backend.logOut();
// backend.logOut();
}
setPassword(undefined);
setBusy(undefined)

View File

@ -36,8 +36,8 @@ export function PublicHistoriesPage({}: Props): VNode {
const result = usePublicAccounts();
const [showAccount, setShowAccount] = useState(
result.ok && result.data.publicAccounts.length > 0
? result.data.publicAccounts[0].accountLabel
result.ok && result.data.public_accounts.length > 0
? result.data.public_accounts[0].account_name
: undefined,
);
@ -51,9 +51,9 @@ export function PublicHistoriesPage({}: Props): VNode {
const accountsBar = [];
// Ask story of all the public accounts.
for (const account of data.publicAccounts) {
logger.trace("Asking transactions for", account.accountLabel);
const isSelected = account.accountLabel == showAccount;
for (const account of data.public_accounts) {
logger.trace("Asking transactions for", account.account_name);
const isSelected = account.account_name == showAccount;
accountsBar.push(
<li
class={
@ -65,13 +65,13 @@ export function PublicHistoriesPage({}: Props): VNode {
<a
href="#"
class="pure-menu-link"
onClick={() => setShowAccount(account.accountLabel)}
onClick={() => setShowAccount(account.account_name)}
>
{account.accountLabel}
{account.account_name}
</a>
</li>,
);
txs[account.accountLabel] = <Transactions account={account.accountLabel} />;
txs[account.account_name] = <Transactions account={account.account_name} />;
}
return (

View File

@ -53,6 +53,7 @@ export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
const backend = useBackendContext();
const [username, setUsername] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
@ -60,6 +61,9 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
name: !name
? i18n.str`Missing name`
: undefined,
username: !username
? i18n.str`Missing username`
: !USERNAME_REGEX.test(username)
@ -74,9 +78,9 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
});
async function doRegistrationStep() {
if (!username || !password) return;
if (!username || !password || !name) return;
try {
await register({ username, password });
await register({ name, username, password });
setUsername(undefined);
backend.logIn({ username, password });
onComplete();
@ -97,13 +101,13 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
setPassword(undefined);
setRepeatPassword(undefined);
}
}
async function delay(ms: number):Promise<void> {
async function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(undefined);
@ -117,14 +121,15 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
await register({ username: user, password: pass });
backend.logIn({ username: user, password: pass });
const username = `_${user.first}-${user.second}_`
await register({ username, name: `${user.first} ${user.second}`, password: pass });
backend.logIn({ username, password: pass });
onComplete();
} catch (error) {
if (error instanceof RequestError) {
if (tries > 0) {
await delay(200)
await doRandomRegistration(tries-1)
await doRandomRegistration(tries - 1)
} else {
notify(
buildRequestErrorMessage(i18n, error.cause, {
@ -142,7 +147,7 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
}
@ -257,17 +262,19 @@ function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
</form>
<p class="mt-10 text-center text-sm text-gray-500 border-t">
<button type="submit"
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
onClick={(e) => {
e.preventDefault()
doRandomRegistration()
}}
>
<i18n.Translate>Create a random user</i18n.Translate>
</button>
</p>
{bankUiSettings.allowRandomAccountCreation &&
<p class="mt-10 text-center text-sm text-gray-500 border-t">
<button type="submit"
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
onClick={(e) => {
e.preventDefault()
doRandomRegistration()
}}
>
<i18n.Translate>Create a random user</i18n.Translate>
</button>
</p>
}
</div>
</div>

View File

@ -59,27 +59,41 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
}, [focus]);
if (!!settings.currentWithdrawalOperationId) {
return <div class="rounded-md bg-yellow-50 ring-yellow-2 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-800">
<i18n.Translate>There is an operation already</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>
To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
</i18n.Translate>
</p>
return <div>
<div class="rounded-md bg-yellow-50 ring-yellow-2 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-800">
<i18n.Translate>There is an operation already</i18n.Translate>
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<i18n.Translate>
To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
</i18n.Translate>
</p>
</div>
</div>
</div>
</div >
<div class="flex justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8 " >
<button type="button" class="text-sm font-semibold leading-6 text-gray-900 bg-white p-2 rounded-sm"
onClick={() => {
updateSettings("currentWithdrawalOperationId", undefined)
onCancel()
}}
>
<i18n.Translate>Cancel</i18n.Translate>
</button>
</div>
</div>
</div >
}
const trimmedAmountStr = amountStr?.trim();

View File

@ -16,9 +16,14 @@ export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode
return handleNotOkResult(i18n, onRegister)(result);
}
const { data } = result;
const balance = Amounts.parseOrThrow(data.balance.amount);
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
//FIXME: libeufin does not follow the spec
const balance = Amounts.parseOrThrow(data.balance);
const balanceIsDebit = true;
// const balance = Amounts.parseOrThrow(data.balance.amount);
// const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;

View File

@ -41,7 +41,9 @@ export function RemoveAccount({
if (focus) ref.current?.focus();
}, [focus]);
const balance = Amounts.parse(result.data.balance.amount);
//FIXME: libeufin does not follow the spec
const balance = Amounts.parse(result.data.balance);
// const balance = Amounts.parse(result.data.balance.amount);
if (!balance) {
return <div>there was an error reading the balance</div>;
}

View File

@ -201,13 +201,13 @@ function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
(result.data.name !== oldResult.name ||
result.data.version !== oldResult.version ||
result.data.ratios_and_fees.buy_at_ratio !==
oldResult.ratios_and_fees.buy_at_ratio ||
oldResult.ratios_and_fees.buy_at_ratio ||
result.data.ratios_and_fees.buy_in_fee !==
oldResult.ratios_and_fees.buy_in_fee ||
oldResult.ratios_and_fees.buy_in_fee ||
result.data.ratios_and_fees.sell_at_ratio !==
oldResult.ratios_and_fees.sell_at_ratio ||
oldResult.ratios_and_fees.sell_at_ratio ||
result.data.ratios_and_fees.sell_out_fee !==
oldResult.ratios_and_fees.sell_out_fee ||
oldResult.ratios_and_fees.sell_out_fee ||
result.data.fiat_currency !== oldResult.fiat_currency);
return {
@ -236,10 +236,14 @@ function CreateCashout({
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
const config = ratiosResult.data;
const balance = Amounts.parseOrThrow(result.data.balance.amount);
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
//FIXME: libeufin does not follow the spec
const balance = Amounts.parseOrThrow(result.data.balance);
const balanceIsDebit = true;
// const balance = Amounts.parseOrThrow(result.data.balance.amount);
// const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
const zero = Amounts.zeroOfCurrency(balance.currency);
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;
@ -250,15 +254,14 @@ function CreateCashout({
const sellFee = !config.ratios_and_fees.sell_out_fee
? zero
: Amounts.parseOrThrow(
`${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
);
`${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
);
const fiatCurrency = config.fiat_currency;
if (!sellRate || sellRate < 0) return <div>error rate</div>;
const amount = Amounts.parseOrThrow(
`${!form.isDebit ? fiatCurrency : balance.currency}:${
!form.amount ? "0" : form.amount
`${!form.isDebit ? fiatCurrency : balance.currency}:${!form.amount ? "0" : form.amount
}`,
);
@ -273,10 +276,10 @@ function CreateCashout({
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
type: "error",
title: i18n.str`Could not estimate the cashout`,
description: error.message as TranslatedString
},
type: "error",
title: i18n.str`Could not estimate the cashout`,
description: error.message as TranslatedString
},
);
});
} else {
@ -289,10 +292,10 @@ function CreateCashout({
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
type: "error",
title: i18n.str`Could not estimate the cashout`,
description: error.message,
},
type: "error",
title: i18n.str`Could not estimate the cashout`,
description: error.message,
},
);
});
}
@ -307,14 +310,14 @@ function CreateCashout({
amount: !form.amount
? i18n.str`required`
: !amount
? i18n.str`could not be parsed`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
: Amounts.cmp(calc.beforeFee, sellFee) === -1
? i18n.str`the total amount to transfer does not cover the fees`
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
? i18n.str`could not be parsed`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
: Amounts.cmp(calc.beforeFee, sellFee) === -1
? i18n.str`the total amount to transfer does not cover the fees`
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
channel: !form.channel ? i18n.str`required` : undefined,
});
@ -341,7 +344,7 @@ function CreateCashout({
{form.isDebit
? i18n.str`Amount to send`
: i18n.str`Amount to receive`}
</label>
<div style={{ display: "flex" }}>
<Amount
@ -520,12 +523,12 @@ function CreateCashout({
status === HttpStatusCode.BadRequest
? i18n.str`The exchange rate was incorrectly applied`
: status === HttpStatusCode.Forbidden
? i18n.str`A institutional user tried the operation`
: status === HttpStatusCode.Conflict
? i18n.str`Need a contact data where to send the TAN`
: status === HttpStatusCode.PreconditionFailed
? i18n.str`The account does not have sufficient funds`
: undefined,
? i18n.str`A institutional user tried the operation`
: status === HttpStatusCode.Conflict
? i18n.str`Need a contact data where to send the TAN`
: status === HttpStatusCode.PreconditionFailed
? i18n.str`The account does not have sufficient funds`
: undefined,
onServerError: (status) =>
status === HttpStatusCode.ServiceUnavailable
? i18n.str`The bank does not support the TAN channel for this operation`
@ -539,7 +542,7 @@ function CreateCashout({
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
}}
>
@ -665,8 +668,8 @@ export function ShowCashoutDetails({
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
? i18n.str`Cashout was already confimed`
: undefined,
? i18n.str`Cashout was already confimed`
: undefined,
}),
);
} else {
@ -676,7 +679,7 @@ export function ShowCashoutDetails({
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
}}
>
@ -702,12 +705,12 @@ export function ShowCashoutDetails({
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
? i18n.str`Cashout was already confimed`
: status === HttpStatusCode.Conflict
? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
: status === HttpStatusCode.Forbidden
? i18n.str`Invalid code`
: undefined,
? i18n.str`Cashout was already confimed`
: status === HttpStatusCode.Conflict
? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
: status === HttpStatusCode.Forbidden
? i18n.str`Invalid code`
: undefined,
}),
);
} else {
@ -717,7 +720,7 @@ export function ShowCashoutDetails({
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
}}
>

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,8 @@ export interface BankUiSettings {
backendBaseURL: string;
allowRegistrations: boolean;
showDemoNav: boolean;
simplePasswordForRandomAccounts: boolean;
allowRandomAccountCreation: boolean;
bankName: string;
demoSites: [string, string][];
}
@ -30,6 +32,8 @@ const defaultSettings: BankUiSettings = {
allowRegistrations: true,
bankName: "Taler Bank",
showDemoNav: true,
simplePasswordForRandomAccounts: true,
allowRandomAccountCreation: true,
demoSites: [
["Landing", "https://demo.taler.net/"],
["Bank", "https://bank.demo.taler.net/"],