This commit is contained in:
Sebastian 2023-09-22 12:41:43 -03:00
parent 56a6f47c7d
commit dfd23f63ba
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
7 changed files with 527 additions and 153 deletions

View File

@ -0,0 +1,117 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank, WithdrawUriResult } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { useComponentState } from "./state.js";
import { ReadyView, AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView } from "./views.js";
import { VNode } from "preact";
import { LoginForm } from "../LoginForm.js";
import { ErrorLoading } from "../../components/ErrorLoading.js";
export interface Props {
currency: string;
onClose: () => void;
}
export type State = State.Loading |
State.LoadingError |
State.Ready |
State.Aborted |
State.Confirmed |
State.InvalidPayto |
State.InvalidWithdrawal |
State.InvalidReserve |
State.NeedConfirmation;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingError {
status: "loading-error";
error: HttpError<SandboxBackend.SandboxError>;
}
/**
* Need to open the wallet
*/
export interface Ready {
status: "ready";
error: undefined;
uri: WithdrawUriResult,
onClose: () => void;
}
export interface InvalidPayto {
status: "invalid-payto",
error: undefined;
payto: string | null;
}
export interface InvalidWithdrawal {
status: "invalid-withdrawal",
error: undefined;
uri: string,
}
export interface InvalidReserve {
status: "invalid-reserve",
error: undefined;
reserve: string | null;
}
export interface NeedConfirmation {
status: "need-confirmation",
error: undefined;
}
export interface Aborted {
status: "aborted",
error: undefined;
onClose: () => void;
}
export interface Confirmed {
status: "confirmed",
error: undefined;
onClose: () => void;
}
}
export interface Transaction {
negative: boolean;
counterpart: string;
when: AbsoluteTime;
amount: AmountJson | undefined;
subject: string;
}
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
"invalid-payto": InvalidPaytoView,
"invalid-withdrawal": InvalidWithdrawalView,
"invalid-reserve": InvalidReserveView,
"need-confirmation": NeedConfirmationView,
"aborted": AbortedView,
"confirmed": ConfirmedView,
"loading-error": ErrorLoading,
ready: ReadyView,
};
export const AccountPage = utils.compose(
(p: Props) => useComponentState(p),
viewMapping,
);

View File

@ -0,0 +1,162 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
import { ErrorType, RequestError, notify, notifyError, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
import { useBackendContext } from "../../context/backend.js";
import { useAccessAPI, useAccountDetails, useWithdrawalDetails } from "../../hooks/access.js";
import { Props, State } from "./index.js";
import { useSettings } from "../../hooks/settings.js";
import { buildRequestErrorMessage } from "../../utils.js";
import { useEffect } from "preact/hooks";
import { getInitialBackendBaseURL } from "../../hooks/backend.js";
export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings()
const { createWithdrawal } = useAccessAPI();
const amount = settings.maxWithdrawalAmount
async function doSilentStart() {
//FIXME: if amount is not enough use balance
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
try {
const result = await createWithdrawal({
amount: Amounts.stringify(parsedAmount),
});
const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
if (!uri) {
return notifyError(
i18n.str`Server responded with an invalid withdraw URI`,
i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
} else {
updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
}
} catch (error) {
if (error instanceof RequestError) {
notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.Forbidden
? i18n.str`The operation was rejected due to insufficient funds`
: undefined,
}),
);
} else {
notifyError(
i18n.str`Operation failed, please report`,
(error instanceof Error
? error.message
: JSON.stringify(error)) as TranslatedString
)
}
}
}
useEffect(() => {
doSilentStart()
}, [settings.fastWithdrawal, amount])
const baseUrl = getInitialBackendBaseURL()
const withdrawalOperationId = settings.currentWithdrawalOperationId
if (!withdrawalOperationId) {
return {
status: "loading",
error: undefined
}
}
const bankIntegrationApiBaseUrl = `${baseUrl}/integration-api`
const uri = stringifyWithdrawUri({
bankIntegrationApiBaseUrl,
withdrawalOperationId,
});
const parsedUri = parseWithdrawUri(uri);
if (!parsedUri) {
return {
status: "invalid-withdrawal",
error: undefined,
uri,
}
}
return (): utils.RecursiveState<State> => {
const result = useWithdrawalDetails(withdrawalOperationId);
if (!result.ok) {
if (result.loading) {
return {
status: "loading",
error: undefined
}
}
return {
status: "loading-error",
error: result
}
}
const { data } = result;
if (data.aborted) {
return {
status: "aborted",
error: undefined,
onClose,
}
}
if (data.confirmation_done) {
return {
status: "confirmed",
error: undefined,
onClose,
}
}
if (!data.selection_done) {
return {
status: "ready",
error: undefined,
uri: parsedUri,
onClose
}
}
if (!data.selected_reserve_pub) {
return {
status: "invalid-reserve",
error: undefined,
reserve: data.selected_reserve_pub
}
}
const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
if (!account) {
return {
status: "invalid-payto",
error: undefined,
payto: data.selected_exchange_account
}
}
return {
status: "need-confirmation",
error: undefined,
}
}
}

View File

@ -0,0 +1,29 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
export default {
title: "operation status page",
};
export const Ready = tests.createExample(ReadyView, {});

View File

@ -0,0 +1,32 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import * as tests from "@gnu-taler/web-util/testing";
import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
import { expect } from "chai";
import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
import { Props } from "./index.js";
import { useComponentState } from "./state.js";
describe("Withdrawal operation states", () => {
it("should do some tests", async () => {
});
});

View File

@ -0,0 +1,65 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Transactions } from "../../components/Transactions/index.js";
import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
import { CopyButton } from "../../components/CopyButton.js";
import { bankUiSettings } from "../../settings.js";
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
import { useSettings } from "../../hooks/settings.js";
export function InvalidPaytoView({ error }: State.InvalidPayto) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function InvalidWithdrawalView({ error }: State.InvalidWithdrawal) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function InvalidReserveView({ error }: State.InvalidReserve) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function NeedConfirmationView({ error }: State.NeedConfirmation) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function AbortedView({ error }: State.Aborted) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function ConfirmedView({ error }: State.Confirmed) {
return (
<div>Payto from server is not valid &quot;{error.data.paytoUri}&quot;</div>
);
}
export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> {
const { i18n } = useTranslationContext();
return <div />
}

View File

@ -6,163 +6,132 @@ import { Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props { interface Props {
onAction: (type: AccountAction, account: string) => void; onAction: (type: AccountAction, account: string) => void;
account: string | undefined; account: string | undefined;
onRegister: () => void; onRegister: () => void;
onCreateAccount: () => void; onCreateAccount: () => void;
} }
export function AccountList({ account, onAction, onCreateAccount, onRegister }: Props): VNode { export function AccountList({ account, onAction, onCreateAccount, onRegister }: Props): VNode {
const result = useBusinessAccounts({ account }); const result = useBusinessAccounts({ account });
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
if (result.loading) return <div />; if (result.loading) return <div />;
if (!result.ok) { if (!result.ok) {
return handleNotOkResult(i18n, onRegister)(result); return handleNotOkResult(i18n, onRegister)(result);
} }
const { customers } = result.data; const { customers } = result.data;
return <div class="px-4 sm:px-6 lg:px-8"> return <div class="px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-base font-semibold leading-6 text-gray-900"> <h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Accounts</i18n.Translate> <i18n.Translate>Accounts</i18n.Translate>
</h1> </h1>
<p class="mt-2 text-sm text-gray-700"> <p class="mt-2 text-sm text-gray-700">
<i18n.Translate>A list of all business account in the bank.</i18n.Translate> <i18n.Translate>A list of all business account in the bank.</i18n.Translate>
</p> </p>
</div> </div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none"> <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<button type="button" class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" <button type="button" class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
onCreateAccount() onCreateAccount()
}}> }}>
<i18n.Translate>Create account</i18n.Translate> <i18n.Translate>Create account</i18n.Translate>
</button> </button>
</div> </div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
{!customers.length ? (
<div></div>
) : (
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">{i18n.str`Actions`}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{customers.map((item, idx) => {
const balance = !item.balance
? undefined
: Amounts.parse(item.balance.amount);
const balanceIsDebit =
item.balance &&
item.balance.credit_debit_indicator == "debit";
return <tr key={idx}>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
{item.username}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{item.name}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{!balance ? (
i18n.str`unknown`
) : (
<span class="amount">
{balanceIsDebit ? <b>-</b> : null}
<span class="value">{`${Amounts.stringifyValue(
balance,
)}`}</span>
&nbsp;
<span class="currency">{`${balance.currency}`}</span>
</span>
)}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
onAction("update-password", item.username)
}}
>
change password
</a>
<br/>
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("show-cashout", item.username)
}}
>
cashouts
</a>
<br/>
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("remove-account", item.username)
}}
>
remove
</a>
</td>
</tr>
})}
{/* <!-- More people... --> */}
</tbody>
</table>
)}
</div>
</div>
</div>
</div> </div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
{!customers.length ? (
<div></div>
) : (
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">{i18n.str`Actions`}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{customers.map((item, idx) => {
const balance = !item.balance
? undefined
: Amounts.parse(item.balance.amount);
const balanceIsDebit =
item.balance &&
item.balance.credit_debit_indicator == "debit";
// return <section return <tr key={idx}>
// id="main" <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
// style={{ width: 600, marginLeft: "auto", marginRight: "auto" }} <a href="#" class="text-indigo-600 hover:text-indigo-900"
// > onClick={(e) => {
// <article> e.preventDefault();
// <h2>{i18n.str`Accounts:`}</h2> onAction("show-details", item.username)
// <div class="results"> }}
// <table class="pure-table pure-table-striped"> >
// <tbody> {item.username}
// return ( </a>
// <tr key={idx}>
// <td>
// <a
// href="#"
// onClick={(e) => {
// e.preventDefault();
// onAction("show-details", item.username)
// }}
// >
// {item.username}
// </a>
// </td>
// <td>{item.name}</td>
// <td>
//
// </td>
// <td>
// </td>
// </tr> </td>
// ); <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
// })} {item.name}
// </tbody> </td>
// </table> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
// </div> {!balance ? (
// </article> i18n.str`unknown`
// )} ) : (
// </section> <span class="amount">
{balanceIsDebit ? <b>-</b> : null}
<span class="value">{`${Amounts.stringifyValue(
balance,
)}`}</span>
&nbsp;
<span class="currency">{`${balance.currency}`}</span>
</span>
)}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a href="#" class="text-indigo-600 hover:text-indigo-900"
onClick={(e) => {
e.preventDefault();
onAction("update-password", item.username)
}}
>
change password
</a>
<br />
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("show-cashout", item.username)
}}
>
cashouts
</a>
<br />
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
e.preventDefault();
onAction("remove-account", item.username)
}}
>
remove
</a>
</td>
</tr>
})}
</tbody>
</table>
)}
</div>
</div>
</div>
</div>
} }

View File

@ -35,7 +35,7 @@ export function AdminHome({ onRegister }: Props): VNode {
if (action) { if (action) {
switch (action.type) { switch (action.type) {
case "show-details": return <ShowCashoutDetails case "show-cashouts-details": return <ShowCashoutDetails
id={action.account} id={action.account}
onLoadNotOk={handleNotOkResult(i18n, onRegister)} onLoadNotOk={handleNotOkResult(i18n, onRegister)}
onCancel={() => { onCancel={() => {
@ -93,7 +93,7 @@ export function AdminHome({ onRegister }: Props): VNode {
setAction(undefined); setAction(undefined);
}} }}
/> />
case "show-cashouts-details": return <ShowAccountDetails case "show-details": return <ShowAccountDetails
account={action.account} account={action.account}
onLoadNotOk={handleNotOkResult(i18n, onRegister)} onLoadNotOk={handleNotOkResult(i18n, onRegister)}
onChangePassword={() => { onChangePassword={() => {