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

View File

@ -99,11 +99,6 @@ type Amount = string;
type UUID = string; type UUID = string;
type Integer = number; type Integer = number;
interface Balance {
amount: Amount;
credit_debit_indicator: "credit" | "debit";
}
namespace SandboxBackend { namespace SandboxBackend {
export interface Config { export interface Config {
// Name of this API, always "circuit". // Name of this API, always "circuit".
@ -126,7 +121,7 @@ namespace SandboxBackend {
} }
export interface SandboxError { export interface SandboxError {
error: SandboxErrorDetail; error?: SandboxErrorDetail;
} }
interface SandboxErrorDetail { interface SandboxErrorDetail {
// String enum classifying the error. // String enum classifying the error.
@ -152,26 +147,12 @@ namespace SandboxBackend {
UtilError = "util-error", 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 { type EmailAddress = string;
// Available balance on the account. type PhoneNumber = string;
balance: Balance;
// payto://-URI of the account. (New) namespace CoreBank {
paytoUri: string;
// Number indicating the max debit allowed for the requesting user.
debitThreshold: Amount;
}
interface BankAccountCreateWithdrawalRequest { interface BankAccountCreateWithdrawalRequest {
// Amount to withdraw. // Amount to withdraw.
amount: Amount; amount: Amount;
@ -243,11 +224,144 @@ namespace SandboxBackend {
amount?: string; amount?: string;
} }
interface BankRegistrationRequest { interface RegisterAccountRequest {
// Username
username: string; username: string;
// Password.
password: string; 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 { namespace Circuit {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ import { useSettings } from "../../hooks/settings.js";
export function InvalidIbanView({ error }: State.InvalidIban) { export function InvalidIbanView({ error }: State.InvalidIban) {
return ( 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); const result = useAccountDetails(account);
if (!result.ok) return <div /> if (!result.ok) return <div />
// const account = "Sebastian" const payto = parsePaytoUri(result.data.payto_uri)
const payto = parsePaytoUri(result.data.paytoUri)
if (!payto) return <div /> if (!payto) return <div />
const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined; const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
return <i18n.Translate> return <i18n.Translate>
Welcome, {account} {accountNumber !== undefined ? Welcome, {account} {accountNumber !== undefined ?
<span> <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> </span>
: <Fragment />}! : <Fragment />}!
</i18n.Translate> </i18n.Translate>
@ -477,9 +476,12 @@ function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account); const result = useAccountDetails(account);
if (!result.ok) return <div /> if (!result.ok) return <div />
// FIXME: balance
return <div> 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" ? "-" : ""} &nbsp;{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""}
{Amounts.stringifyValue(result.data.balance.amount)} {Amounts.stringifyValue(result.data.balance.amount)} */}
</div> </div>
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -59,27 +59,41 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
}, [focus]); }, [focus]);
if (!!settings.currentWithdrawalOperationId) { if (!!settings.currentWithdrawalOperationId) {
return <div class="rounded-md bg-yellow-50 ring-yellow-2 p-4"> return <div>
<div class="flex">
<div class="flex-shrink-0"> <div class="rounded-md bg-yellow-50 ring-yellow-2 p-4">
<svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <div class="flex">
<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" /> <div class="flex-shrink-0">
</svg> <svg class="h-5 w-5 text-yellow-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
</div> <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" />
<div class="ml-3"> </svg>
<h3 class="text-sm font-bold text-yellow-800"> </div>
<i18n.Translate>There is an operation already</i18n.Translate> <div class="ml-3">
</h3> <h3 class="text-sm font-bold text-yellow-800">
<div class="mt-2 text-sm text-yellow-700"> <i18n.Translate>There is an operation already</i18n.Translate>
<p> </h3>
<i18n.Translate> <div class="mt-2 text-sm text-yellow-700">
To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a> <p>
</i18n.Translate> <i18n.Translate>
</p> 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 >
<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> </div>
</div >
} }
const trimmedAmountStr = amountStr?.trim(); const trimmedAmountStr = amountStr?.trim();

View File

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

View File

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