cashout for business accounts
This commit is contained in:
parent
8b83f729d7
commit
9697e953f5
@ -23,7 +23,8 @@ import { useComponentState } from "./state.js";
|
|||||||
import { LoadingUriView, ReadyView } from "./views.js";
|
import { LoadingUriView, ReadyView } from "./views.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
empty?: boolean;
|
account: string;
|
||||||
|
onSelected: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading | State.LoadingUriError | State.Ready;
|
export type State = State.Loading | State.LoadingUriError | State.Ready;
|
||||||
@ -45,7 +46,8 @@ export namespace State {
|
|||||||
export interface Ready extends BaseInfo {
|
export interface Ready extends BaseInfo {
|
||||||
status: "ready";
|
status: "ready";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
cashouts: SandboxBackend.Circuit.CashoutStatusResponse[];
|
cashouts: SandboxBackend.Circuit.CashoutStatusResponseWithId[];
|
||||||
|
onSelected: (id: string) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,12 +14,11 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
|
|
||||||
import { useCashouts } from "../../hooks/circuit.js";
|
import { useCashouts } from "../../hooks/circuit.js";
|
||||||
import { Props, State, Transaction } from "./index.js";
|
import { Props, State } from "./index.js";
|
||||||
|
|
||||||
export function useComponentState({ empty }: Props): State {
|
export function useComponentState({ account, onSelected }: Props): State {
|
||||||
const result = useCashouts();
|
const result = useCashouts(account);
|
||||||
if (result.loading) {
|
if (result.loading) {
|
||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
@ -37,5 +36,6 @@ export function useComponentState({ empty }: Props): State {
|
|||||||
status: "ready",
|
status: "ready",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
cashouts: result.data,
|
cashouts: result.data,
|
||||||
|
onSelected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ describe("Transaction states", () => {
|
|||||||
const env = new SwrMockEnvironment();
|
const env = new SwrMockEnvironment();
|
||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
|
account: "123",
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
|
||||||
@ -115,6 +115,7 @@ describe("Transaction states", () => {
|
|||||||
const env = new SwrMockEnvironment();
|
const env = new SwrMockEnvironment();
|
||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
|
account: "123",
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
|
||||||
@ -147,6 +148,7 @@ describe("Transaction states", () => {
|
|||||||
const env = new SwrMockEnvironment(false);
|
const env = new SwrMockEnvironment(false);
|
||||||
|
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
|
account: "123",
|
||||||
};
|
};
|
||||||
|
|
||||||
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
|
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
|
||||||
|
@ -30,8 +30,15 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadyView({ cashouts }: State.Ready): VNode {
|
export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
if (!cashouts.length) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<i18n.Translate>No cashout at the moment</i18n.Translate>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div class="results">
|
<div class="results">
|
||||||
<table class="pure-table pure-table-striped">
|
<table class="pure-table pure-table-striped">
|
||||||
@ -39,6 +46,8 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{i18n.str`Created`}</th>
|
<th>{i18n.str`Created`}</th>
|
||||||
<th>{i18n.str`Confirmed`}</th>
|
<th>{i18n.str`Confirmed`}</th>
|
||||||
|
<th>{i18n.str`Total debit`}</th>
|
||||||
|
<th>{i18n.str`Total credit`}</th>
|
||||||
<th>{i18n.str`Status`}</th>
|
<th>{i18n.str`Status`}</th>
|
||||||
<th>{i18n.str`Subject`}</th>
|
<th>{i18n.str`Subject`}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -56,7 +65,17 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
|
|||||||
<td>{Amounts.stringifyValue(item.amount_debit)}</td>
|
<td>{Amounts.stringifyValue(item.amount_debit)}</td>
|
||||||
<td>{Amounts.stringifyValue(item.amount_credit)}</td>
|
<td>{Amounts.stringifyValue(item.amount_credit)}</td>
|
||||||
<td>{item.status}</td>
|
<td>{item.status}</td>
|
||||||
<td>{item.subject}</td>
|
<td>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelected(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.subject}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
16
packages/demobank-ui/src/declaration.d.ts
vendored
16
packages/demobank-ui/src/declaration.d.ts
vendored
@ -322,11 +322,6 @@ namespace SandboxBackend {
|
|||||||
// where to send cashouts.
|
// where to send cashouts.
|
||||||
cashout_address: string;
|
cashout_address: string;
|
||||||
}
|
}
|
||||||
enum TanChannel {
|
|
||||||
SMS = "sms",
|
|
||||||
EMAIL = "email",
|
|
||||||
FILE = "file",
|
|
||||||
}
|
|
||||||
interface CashoutRequest {
|
interface CashoutRequest {
|
||||||
// Optional subject to associate to the
|
// Optional subject to associate to the
|
||||||
// cashout operation. This data will appear
|
// cashout operation. This data will appear
|
||||||
@ -369,6 +364,7 @@ namespace SandboxBackend {
|
|||||||
// Contains ratios and fees related to buying
|
// Contains ratios and fees related to buying
|
||||||
// and selling the circuit currency.
|
// and selling the circuit currency.
|
||||||
ratios_and_fees: RatiosAndFees;
|
ratios_and_fees: RatiosAndFees;
|
||||||
|
currency: string;
|
||||||
}
|
}
|
||||||
interface RatiosAndFees {
|
interface RatiosAndFees {
|
||||||
// Exchange rate to buy the circuit currency from fiat.
|
// Exchange rate to buy the circuit currency from fiat.
|
||||||
@ -400,14 +396,6 @@ namespace SandboxBackend {
|
|||||||
// Missing or null, when the operation wasn't confirmed yet.
|
// Missing or null, when the operation wasn't confirmed yet.
|
||||||
confirmation_time?: number | null; // milliseconds since the Unix epoch
|
confirmation_time?: number | null; // milliseconds since the Unix epoch
|
||||||
}
|
}
|
||||||
enum CashoutStatus {
|
type CashoutStatusResponseWithId = CashoutStatusResponse & { id: string };
|
||||||
// The payment was initiated after a valid
|
|
||||||
// TAN was received by the bank.
|
|
||||||
CONFIRMED = "confirmed",
|
|
||||||
|
|
||||||
// The cashout was created and now waits
|
|
||||||
// for the TAN by the author.
|
|
||||||
PENDING = "pending",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseOk,
|
HttpResponseOk,
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError
|
RequestError,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
@ -26,12 +26,12 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
|
|||||||
import {
|
import {
|
||||||
useAuthenticatedBackend,
|
useAuthenticatedBackend,
|
||||||
useMatchMutate,
|
useMatchMutate,
|
||||||
usePublicBackend
|
usePublicBackend,
|
||||||
} from "./backend.js";
|
} from "./backend.js";
|
||||||
|
|
||||||
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
import _useSWR, { SWRHook } from 'swr';
|
import _useSWR, { SWRHook } from "swr";
|
||||||
const useSWR = _useSWR as unknown as SWRHook
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
export function useAccessAPI(): AccessAPI {
|
export function useAccessAPI(): AccessAPI {
|
||||||
const mutateAll = useMatchMutate();
|
const mutateAll = useMatchMutate();
|
||||||
|
@ -118,6 +118,7 @@ interface useBackendType {
|
|||||||
sandboxAccountsFetcher: <T>(
|
sandboxAccountsFetcher: <T>(
|
||||||
args: [string, number, number, string],
|
args: [string, number, number, string],
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
|
sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
|
||||||
}
|
}
|
||||||
export function usePublicBackend(): useBackendType {
|
export function usePublicBackend(): useBackendType {
|
||||||
const { state } = useBackendContext();
|
const { state } = useBackendContext();
|
||||||
@ -176,12 +177,21 @@ export function usePublicBackend(): useBackendType {
|
|||||||
},
|
},
|
||||||
[baseUrl],
|
[baseUrl],
|
||||||
);
|
);
|
||||||
|
const sandboxCashoutFetcher = useCallback(
|
||||||
|
function fetcherImpl<T>([endpoint, account]: string[]): Promise<
|
||||||
|
HttpResponseOk<T>
|
||||||
|
> {
|
||||||
|
return requestHandler<T>(baseUrl, endpoint);
|
||||||
|
},
|
||||||
|
[baseUrl],
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
fetcher,
|
fetcher,
|
||||||
paginatedFetcher,
|
paginatedFetcher,
|
||||||
multiFetcher,
|
multiFetcher,
|
||||||
sandboxAccountsFetcher,
|
sandboxAccountsFetcher,
|
||||||
|
sandboxCashoutFetcher,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +235,6 @@ export function useAuthenticatedBackend(): useBackendType {
|
|||||||
function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
|
function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
|
||||||
HttpResponseOk<T>[]
|
HttpResponseOk<T>[]
|
||||||
> {
|
> {
|
||||||
console.log("list size", endpoints.length, endpoints);
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
endpoints.map((endpoint) =>
|
endpoints.map((endpoint) =>
|
||||||
requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
|
requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
|
||||||
@ -249,12 +258,24 @@ export function useAuthenticatedBackend(): useBackendType {
|
|||||||
[baseUrl],
|
[baseUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sandboxCashoutFetcher = useCallback(
|
||||||
|
function fetcherImpl<T>([endpoint, account]: string[]): Promise<
|
||||||
|
HttpResponseOk<T>
|
||||||
|
> {
|
||||||
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
|
basicAuth: creds,
|
||||||
|
params: { account },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[baseUrl, creds],
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
fetcher,
|
fetcher,
|
||||||
paginatedFetcher,
|
paginatedFetcher,
|
||||||
multiFetcher,
|
multiFetcher,
|
||||||
sandboxAccountsFetcher,
|
sandboxAccountsFetcher,
|
||||||
|
sandboxCashoutFetcher,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
|
|||||||
import { useAuthenticatedBackend, useMatchMutate } from "./backend.js";
|
import { useAuthenticatedBackend, useMatchMutate } from "./backend.js";
|
||||||
|
|
||||||
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
import _useSWR, { SWRHook } from 'swr';
|
import _useSWR, { SWRHook } from "swr";
|
||||||
const useSWR = _useSWR as unknown as SWRHook
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
export function useAdminAccountAPI(): AdminAccountAPI {
|
export function useAdminAccountAPI(): AdminAccountAPI {
|
||||||
const { request } = useAuthenticatedBackend();
|
const { request } = useAuthenticatedBackend();
|
||||||
@ -118,7 +118,54 @@ export function useCircuitAccountAPI(): CircuitAccountAPI {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { updateAccount, changePassword };
|
const createCashout = async (
|
||||||
|
data: SandboxBackend.Circuit.CashoutRequest,
|
||||||
|
): Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>> => {
|
||||||
|
const res = await request<SandboxBackend.Circuit.CashoutPending>(
|
||||||
|
`circuit-api/cashouts`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
contentType: "json",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCashout = async (
|
||||||
|
cashoutId: string,
|
||||||
|
data: SandboxBackend.Circuit.CashoutConfirm,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
const res = await request<void>(
|
||||||
|
`circuit-api/cashouts/${cashoutId}/confirm`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
contentType: "json",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await mutateAll(/.*circuit-api\/cashout.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortCashout = async (
|
||||||
|
cashoutId: string,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
const res = await request<void>(`circuit-api/cashouts/${cashoutId}/abort`, {
|
||||||
|
method: "POST",
|
||||||
|
contentType: "json",
|
||||||
|
});
|
||||||
|
await mutateAll(/.*circuit-api\/cashout.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAccount,
|
||||||
|
changePassword,
|
||||||
|
createCashout,
|
||||||
|
confirmCashout,
|
||||||
|
abortCashout,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminAccountAPI {
|
export interface AdminAccountAPI {
|
||||||
@ -144,11 +191,14 @@ export interface CircuitAccountAPI {
|
|||||||
changePassword: (
|
changePassword: (
|
||||||
data: SandboxBackend.Circuit.AccountPasswordChange,
|
data: SandboxBackend.Circuit.AccountPasswordChange,
|
||||||
) => Promise<HttpResponseOk<void>>;
|
) => Promise<HttpResponseOk<void>>;
|
||||||
}
|
createCashout: (
|
||||||
|
data: SandboxBackend.Circuit.CashoutRequest,
|
||||||
export interface InstanceTemplateFilter {
|
) => Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>>;
|
||||||
//FIXME: add filter to the template list
|
confirmCashout: (
|
||||||
position?: string;
|
id: string,
|
||||||
|
data: SandboxBackend.Circuit.CashoutConfirm,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
abortCashout: (id: string) => Promise<HttpResponseOk<void>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBusinessStatus(
|
async function getBusinessStatus(
|
||||||
@ -217,6 +267,35 @@ export function useBusinessAccountDetails(
|
|||||||
return { loading: true };
|
return { loading: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRatiosAndFeeConfig(): HttpResponse<
|
||||||
|
SandboxBackend.Circuit.Config,
|
||||||
|
SandboxBackend.SandboxError
|
||||||
|
> {
|
||||||
|
const { fetcher } = useAuthenticatedBackend();
|
||||||
|
|
||||||
|
const { data, error } = useSWR<
|
||||||
|
HttpResponseOk<SandboxBackend.Circuit.Config>,
|
||||||
|
RequestError<SandboxBackend.SandboxError>
|
||||||
|
>([`circuit-api/config`], fetcher, {
|
||||||
|
refreshInterval: 0,
|
||||||
|
refreshWhenHidden: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshWhenOffline: false,
|
||||||
|
errorRetryCount: 0,
|
||||||
|
errorRetryInterval: 1,
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
data.data.currency = "FIAT";
|
||||||
|
}
|
||||||
|
if (data) return data;
|
||||||
|
if (error) return error.info;
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
interface PaginationFilter {
|
interface PaginationFilter {
|
||||||
account?: string;
|
account?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
@ -299,17 +378,18 @@ export function useBusinessAccounts(
|
|||||||
return { loading: true };
|
return { loading: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCashouts(): HttpResponse<
|
export function useCashouts(
|
||||||
(SandboxBackend.Circuit.CashoutStatusResponse & WithId)[],
|
account: string,
|
||||||
|
): HttpResponse<
|
||||||
|
SandboxBackend.Circuit.CashoutStatusResponseWithId[],
|
||||||
SandboxBackend.SandboxError
|
SandboxBackend.SandboxError
|
||||||
> {
|
> {
|
||||||
const { fetcher, multiFetcher } = useAuthenticatedBackend();
|
const { sandboxCashoutFetcher, multiFetcher } = useAuthenticatedBackend();
|
||||||
|
|
||||||
|
|
||||||
const { data: list, error: listError } = useSWR<
|
const { data: list, error: listError } = useSWR<
|
||||||
HttpResponseOk<SandboxBackend.Circuit.Cashouts>,
|
HttpResponseOk<SandboxBackend.Circuit.Cashouts>,
|
||||||
RequestError<SandboxBackend.SandboxError>
|
RequestError<SandboxBackend.SandboxError>
|
||||||
>([`circuit-api/cashouts`], fetcher, {
|
>([`circuit-api/cashouts`, account], sandboxCashoutFetcher, {
|
||||||
refreshInterval: 0,
|
refreshInterval: 0,
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@ -317,7 +397,7 @@ export function useCashouts(): HttpResponse<
|
|||||||
refreshWhenOffline: false,
|
refreshWhenOffline: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const paths = (list?.data.cashouts || []).map(
|
const paths = ((list?.data && list?.data.cashouts) || []).map(
|
||||||
(cashoutId) => `circuit-api/cashouts/${cashoutId}`,
|
(cashoutId) => `circuit-api/cashouts/${cashoutId}`,
|
||||||
);
|
);
|
||||||
const { data: cashouts, error: productError } = useSWR<
|
const { data: cashouts, error: productError } = useSWR<
|
||||||
@ -346,3 +426,31 @@ export function useCashouts(): HttpResponse<
|
|||||||
}
|
}
|
||||||
return { loading: true };
|
return { loading: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCashoutDetails(
|
||||||
|
id: string,
|
||||||
|
): HttpResponse<
|
||||||
|
SandboxBackend.Circuit.CashoutStatusResponse,
|
||||||
|
SandboxBackend.SandboxError
|
||||||
|
> {
|
||||||
|
const { fetcher } = useAuthenticatedBackend();
|
||||||
|
|
||||||
|
const { data, error } = useSWR<
|
||||||
|
HttpResponseOk<SandboxBackend.Circuit.CashoutStatusResponse>,
|
||||||
|
RequestError<SandboxBackend.SandboxError>
|
||||||
|
>([`circuit-api/cashouts/${id}`], fetcher, {
|
||||||
|
refreshInterval: 0,
|
||||||
|
refreshWhenHidden: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshWhenOffline: false,
|
||||||
|
errorRetryCount: 0,
|
||||||
|
errorRetryInterval: 1,
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) return data;
|
||||||
|
if (error) return error.info;
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
@ -112,40 +112,3 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function Moves({ account }: { account: string }): VNode {
|
|
||||||
// const [tab, setTab] = useState<"transactions" | "cashouts">("transactions");
|
|
||||||
// const { i18n } = useTranslationContext();
|
|
||||||
// return (
|
|
||||||
// <article>
|
|
||||||
// <div class="payments">
|
|
||||||
// <div class="tab">
|
|
||||||
// <button
|
|
||||||
// class={tab === "transactions" ? "tablinks active" : "tablinks"}
|
|
||||||
// onClick={(): void => {
|
|
||||||
// setTab("transactions");
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {i18n.str`Transactions`}
|
|
||||||
// </button>
|
|
||||||
// <button
|
|
||||||
// class={tab === "cashouts" ? "tablinks active" : "tablinks"}
|
|
||||||
// onClick={(): void => {
|
|
||||||
// setTab("cashouts");
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {i18n.str`Cashouts`}
|
|
||||||
// </button>
|
|
||||||
// </div>
|
|
||||||
// {tab === "transactions" && (
|
|
||||||
// )}
|
|
||||||
// {tab === "cashouts" && (
|
|
||||||
// <div class="active">
|
|
||||||
// <h3>{i18n.str`Latest cashouts`}</h3>
|
|
||||||
// <Cashouts account={account} />
|
|
||||||
// </div>
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// </article>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
@ -14,7 +14,11 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parsePaytoUri, TranslatedString } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
Amounts,
|
||||||
|
parsePaytoUri,
|
||||||
|
TranslatedString,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
RequestError,
|
RequestError,
|
||||||
@ -22,7 +26,9 @@ import {
|
|||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { Cashouts } from "../components/Cashouts/index.js";
|
||||||
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
||||||
|
import { useAccountDetails } from "../hooks/access.js";
|
||||||
import {
|
import {
|
||||||
useBusinessAccountDetails,
|
useBusinessAccountDetails,
|
||||||
useBusinessAccounts,
|
useBusinessAccounts,
|
||||||
@ -60,7 +66,10 @@ interface Props {
|
|||||||
export function AdminPage({ onLoadNotOk }: Props): VNode {
|
export function AdminPage({ onLoadNotOk }: Props): VNode {
|
||||||
const [account, setAccount] = useState<string | undefined>();
|
const [account, setAccount] = useState<string | undefined>();
|
||||||
const [showDetails, setShowDetails] = useState<string | undefined>();
|
const [showDetails, setShowDetails] = useState<string | undefined>();
|
||||||
|
const [showCashouts, setShowCashouts] = useState<string | undefined>();
|
||||||
const [updatePassword, setUpdatePassword] = useState<string | undefined>();
|
const [updatePassword, setUpdatePassword] = useState<string | undefined>();
|
||||||
|
const [removeAccount, setRemoveAccount] = useState<string | undefined>();
|
||||||
|
|
||||||
const [createAccount, setCreateAccount] = useState(false);
|
const [createAccount, setCreateAccount] = useState(false);
|
||||||
const { pageStateSetter } = usePageContext();
|
const { pageStateSetter } = usePageContext();
|
||||||
|
|
||||||
@ -81,6 +90,23 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|||||||
|
|
||||||
const { customers } = result.data;
|
const { customers } = result.data;
|
||||||
|
|
||||||
|
if (showCashouts) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Cashouts account={showCashouts} />
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowCashouts(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (showDetails) {
|
if (showDetails) {
|
||||||
return (
|
return (
|
||||||
<ShowAccountDetails
|
<ShowAccountDetails
|
||||||
@ -100,6 +126,21 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (removeAccount) {
|
||||||
|
return (
|
||||||
|
<RemoveAccount
|
||||||
|
account={removeAccount}
|
||||||
|
onLoadNotOk={onLoadNotOk}
|
||||||
|
onUpdateSuccess={() => {
|
||||||
|
showInfoMessage(i18n.str`Account removed`);
|
||||||
|
setRemoveAccount(undefined);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setRemoveAccount(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (updatePassword) {
|
if (updatePassword) {
|
||||||
return (
|
return (
|
||||||
<UpdateAccountPassword
|
<UpdateAccountPassword
|
||||||
@ -164,6 +205,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|||||||
<th>{i18n.str`Username`}</th>
|
<th>{i18n.str`Username`}</th>
|
||||||
<th>{i18n.str`Name`}</th>
|
<th>{i18n.str`Name`}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -193,6 +235,28 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|||||||
change password
|
change password
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowCashouts(item.username);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cashouts
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setRemoveAccount(item.username);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
remove
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -536,6 +600,90 @@ export function ShowAccountDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RemoveAccount({
|
||||||
|
account,
|
||||||
|
onClear,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onLoadNotOk,
|
||||||
|
}: {
|
||||||
|
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
|
||||||
|
onClear: () => void;
|
||||||
|
onUpdateSuccess: () => void;
|
||||||
|
account: string;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useAccountDetails(account);
|
||||||
|
const { deleteAccount } = useAdminAccountAPI();
|
||||||
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||||
|
|
||||||
|
if (result.clientError) {
|
||||||
|
if (result.isNotfound) return <div>account not found</div>;
|
||||||
|
}
|
||||||
|
if (!result.ok) {
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = Amounts.parse(result.data.balance.amount);
|
||||||
|
if (!balance) {
|
||||||
|
return <div>there was an error reading the balance</div>;
|
||||||
|
}
|
||||||
|
const isBalanceEmpty = Amounts.isZero(balance);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{!isBalanceEmpty && (
|
||||||
|
<ErrorBanner
|
||||||
|
error={{
|
||||||
|
title: i18n.str`Can't delete the account`,
|
||||||
|
description: i18n.str`Balance is not empty`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Cancel`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={!isBalanceEmpty}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Confirm`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const r = await deleteAccount(account);
|
||||||
|
onUpdateSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, saveError, i18n);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create valid account object to update or create
|
* Create valid account object to update or create
|
||||||
* Take template as initial values for the form
|
* Take template as initial values for the form
|
||||||
|
@ -128,7 +128,7 @@ export function BankFrame({
|
|||||||
<StatusBanner />
|
<StatusBanner />
|
||||||
{backend.state.status === "loggedIn" ? (
|
{backend.state.status === "loggedIn" ? (
|
||||||
<div class="top-right">
|
<div class="top-right">
|
||||||
{goToBusinessAccount ? (
|
{goToBusinessAccount && !backend.state.isUserAdministrator ? (
|
||||||
<MaybeBusinessButton
|
<MaybeBusinessButton
|
||||||
account={backend.state.username}
|
account={backend.state.username}
|
||||||
onClick={goToBusinessAccount}
|
onClick={goToBusinessAccount}
|
||||||
@ -187,7 +187,7 @@ export function ErrorBanner({
|
|||||||
onClear,
|
onClear,
|
||||||
}: {
|
}: {
|
||||||
error: ErrorMessage;
|
error: ErrorMessage;
|
||||||
onClear: () => void;
|
onClear?: () => void;
|
||||||
}): VNode | null {
|
}): VNode | null {
|
||||||
return (
|
return (
|
||||||
<div class="informational informational-fail" style={{ marginTop: 8 }}>
|
<div class="informational informational-fail" style={{ marginTop: 8 }}>
|
||||||
@ -196,15 +196,17 @@ export function ErrorBanner({
|
|||||||
<b>{error.title}</b>
|
<b>{error.title}</b>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<input
|
{onClear && (
|
||||||
type="button"
|
<input
|
||||||
class="pure-button"
|
type="button"
|
||||||
value="Clear"
|
class="pure-button"
|
||||||
onClick={(e) => {
|
value="Clear"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
onClear();
|
e.preventDefault();
|
||||||
}}
|
onClear();
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>{error.description}</p>
|
<p>{error.description}</p>
|
||||||
|
@ -13,18 +13,34 @@
|
|||||||
You should have received a copy of the GNU General Public License along with
|
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/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
import { TranslatedString } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
HttpStatusCode,
|
||||||
|
TranslatedString,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
HttpResponsePaginated,
|
HttpResponsePaginated,
|
||||||
|
RequestError,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Cashouts } from "../components/Cashouts/index.js";
|
import { Cashouts } from "../components/Cashouts/index.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { usePageContext } from "../context/pageState.js";
|
import { ErrorMessage, usePageContext } from "../context/pageState.js";
|
||||||
|
import { useAccountDetails } from "../hooks/access.js";
|
||||||
|
import {
|
||||||
|
useCashoutDetails,
|
||||||
|
useCashouts,
|
||||||
|
useCircuitAccountAPI,
|
||||||
|
useRatiosAndFeeConfig,
|
||||||
|
} from "../hooks/circuit.js";
|
||||||
|
import { CashoutStatus, TanChannel, undefinedIfEmpty } from "../utils.js";
|
||||||
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
||||||
|
import { ErrorBanner } from "./BankFrame.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
|
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -40,6 +56,8 @@ export function BusinessAccount({
|
|||||||
const { pageStateSetter } = usePageContext();
|
const { pageStateSetter } = usePageContext();
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
const [updatePassword, setUpdatePassword] = useState(false);
|
const [updatePassword, setUpdatePassword] = useState(false);
|
||||||
|
const [newCashout, setNewcashout] = useState(false);
|
||||||
|
const [showCashout, setShowCashout] = useState<string | undefined>();
|
||||||
function showInfoMessage(info: TranslatedString): void {
|
function showInfoMessage(info: TranslatedString): void {
|
||||||
pageStateSetter((prev) => ({
|
pageStateSetter((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -51,6 +69,32 @@ export function BusinessAccount({
|
|||||||
return <LoginForm onRegister={onRegister} />;
|
return <LoginForm onRegister={onRegister} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newCashout) {
|
||||||
|
return (
|
||||||
|
<CreateCashout
|
||||||
|
account={backend.state.username}
|
||||||
|
onLoadNotOk={onLoadNotOk}
|
||||||
|
onCancel={() => {
|
||||||
|
setNewcashout(false);
|
||||||
|
}}
|
||||||
|
onComplete={(id) => {
|
||||||
|
setNewcashout(false);
|
||||||
|
setShowCashout(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showCashout) {
|
||||||
|
return (
|
||||||
|
<ShowCashout
|
||||||
|
id={showCashout}
|
||||||
|
onLoadNotOk={onLoadNotOk}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowCashout(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (updatePassword) {
|
if (updatePassword) {
|
||||||
return (
|
return (
|
||||||
<UpdateAccountPassword
|
<UpdateAccountPassword
|
||||||
@ -82,9 +126,634 @@ export function BusinessAccount({
|
|||||||
<section style={{ marginTop: "2em" }}>
|
<section style={{ marginTop: "2em" }}>
|
||||||
<div class="active">
|
<div class="active">
|
||||||
<h3>{i18n.str`Latest cashouts`}</h3>
|
<h3>{i18n.str`Latest cashouts`}</h3>
|
||||||
<Cashouts />
|
<Cashouts
|
||||||
|
account={backend.state.username}
|
||||||
|
onSelected={(id) => {
|
||||||
|
setShowCashout(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div />
|
||||||
|
<input
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`New cashout`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setNewcashout(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PropsCashout {
|
||||||
|
account: string;
|
||||||
|
onComplete: (id: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormType = {
|
||||||
|
isDebit: boolean;
|
||||||
|
amount: string;
|
||||||
|
subject: string;
|
||||||
|
channel: TanChannel;
|
||||||
|
};
|
||||||
|
type ErrorFrom<T> = {
|
||||||
|
[P in keyof T]+?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CreateCashout({
|
||||||
|
account,
|
||||||
|
onComplete,
|
||||||
|
onCancel,
|
||||||
|
onLoadNotOk,
|
||||||
|
}: PropsCashout): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const ratiosResult = useRatiosAndFeeConfig();
|
||||||
|
const result = useAccountDetails(account);
|
||||||
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||||
|
|
||||||
|
const [form, setForm] = useState<Partial<FormType>>({});
|
||||||
|
|
||||||
|
const { createCashout } = useCircuitAccountAPI();
|
||||||
|
if (!result.ok) return onLoadNotOk(result);
|
||||||
|
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
|
||||||
|
const config = ratiosResult.data;
|
||||||
|
const maybeBalance = Amounts.parse(result.data.balance.amount);
|
||||||
|
if (!maybeBalance) return <div>error</div>;
|
||||||
|
const balance = maybeBalance;
|
||||||
|
const zero = Amounts.zeroOfCurrency(balance.currency);
|
||||||
|
|
||||||
|
const sellRate = config.ratios_and_fees.sell_at_ratio;
|
||||||
|
const sellFee = !config.ratios_and_fees.sell_out_fee
|
||||||
|
? zero
|
||||||
|
: Amounts.fromFloat(config.ratios_and_fees.sell_out_fee, balance.currency);
|
||||||
|
|
||||||
|
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
||||||
|
|
||||||
|
function truncate(a: AmountJson): AmountJson {
|
||||||
|
const str = Amounts.stringify(a);
|
||||||
|
const idx = str.indexOf(".");
|
||||||
|
if (idx === -1) return a;
|
||||||
|
const truncated = str.substring(0, idx + 3);
|
||||||
|
console.log(str, truncated);
|
||||||
|
return Amounts.parseOrThrow(truncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
|
||||||
|
const amount_debit = !amount
|
||||||
|
? zero
|
||||||
|
: form.isDebit
|
||||||
|
? amount
|
||||||
|
: truncate(Amounts.divide(Amounts.add(amount, sellFee).amount, sellRate));
|
||||||
|
const credit_before_fee = !amount
|
||||||
|
? zero
|
||||||
|
: form.isDebit
|
||||||
|
? truncate(Amounts.divide(amount, 1 / sellRate))
|
||||||
|
: Amounts.add(amount, sellFee).amount;
|
||||||
|
|
||||||
|
const __amount_credit = Amounts.sub(credit_before_fee, sellFee).amount;
|
||||||
|
const amount_credit = Amounts.parseOrThrow(
|
||||||
|
`${config.currency}:${Amounts.stringifyValue(__amount_credit)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const balanceAfter = Amounts.sub(balance, amount_debit).amount;
|
||||||
|
|
||||||
|
function updateForm(newForm: typeof form): void {
|
||||||
|
setForm(newForm);
|
||||||
|
}
|
||||||
|
const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
|
||||||
|
amount: !form.amount
|
||||||
|
? i18n.str`required`
|
||||||
|
: !amount
|
||||||
|
? i18n.str`could not be parsed`
|
||||||
|
: Amounts.cmp(balance, amount_debit) === -1
|
||||||
|
? i18n.str`balance is not enough`
|
||||||
|
: Amounts.cmp(credit_before_fee, sellFee) === -1
|
||||||
|
? i18n.str`amount is not enough`
|
||||||
|
: Amounts.isZero(amount_credit)
|
||||||
|
? i18n.str`amount is not enough`
|
||||||
|
: undefined,
|
||||||
|
channel: !form.channel ? i18n.str`required` : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// setErrors(validationResult);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error && (
|
||||||
|
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
||||||
|
)}
|
||||||
|
<h1>New cashout</h1>
|
||||||
|
<form class="pure-form">
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Subject`}</label>
|
||||||
|
<input
|
||||||
|
value={form.subject ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.subject = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.subject}
|
||||||
|
isDirty={form.subject !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
{form.isDebit
|
||||||
|
? i18n.str`Amount to send`
|
||||||
|
: i18n.str`Amount to receive`}
|
||||||
|
</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
// ref={ref}
|
||||||
|
id="withdraw-amount"
|
||||||
|
name="withdraw-amount"
|
||||||
|
value={form.amount ?? ""}
|
||||||
|
onChange={(e): void => {
|
||||||
|
form.amount = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="toggle">
|
||||||
|
<input
|
||||||
|
class="toggle-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={(e): void => {
|
||||||
|
form.isDebit = !form.isDebit;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="toggle-switch"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.amount}
|
||||||
|
isDirty={form.amount !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Conversion rate`}</label>
|
||||||
|
<input value={sellRate} disabled />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Balance now`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="withdraw-amount"
|
||||||
|
disabled
|
||||||
|
name="withdraw-amount"
|
||||||
|
value={Amounts.stringifyValue(balance)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label
|
||||||
|
style={{ fontWeight: "bold", color: "red" }}
|
||||||
|
>{i18n.str`Total cost`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
// ref={ref}
|
||||||
|
id="withdraw-amount"
|
||||||
|
disabled
|
||||||
|
name="withdraw-amount"
|
||||||
|
value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Balance after`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
// ref={ref}
|
||||||
|
id="withdraw-amount"
|
||||||
|
disabled
|
||||||
|
name="withdraw-amount"
|
||||||
|
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>{" "}
|
||||||
|
{Amounts.isZero(sellFee) ? undefined : (
|
||||||
|
<Fragment>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Transfer before fee`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
// type="number"
|
||||||
|
style={{ color: "black" }}
|
||||||
|
disabled
|
||||||
|
value={Amounts.stringifyValue(credit_before_fee)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Cashout fee`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
// type="number"
|
||||||
|
style={{ color: "black" }}
|
||||||
|
disabled
|
||||||
|
value={Amounts.stringifyValue(sellFee)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<fieldset>
|
||||||
|
<label
|
||||||
|
style={{ fontWeight: "bold", color: "green" }}
|
||||||
|
>{i18n.str`Total cashout transfer`}</label>
|
||||||
|
<div style={{ width: "max-content" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="currency-indicator"
|
||||||
|
size={balance.currency.length}
|
||||||
|
maxLength={balance.currency.length}
|
||||||
|
tabIndex={-1}
|
||||||
|
value={balance.currency}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
// ref={ref}
|
||||||
|
id="withdraw-amount"
|
||||||
|
disabled
|
||||||
|
name="withdraw-amount"
|
||||||
|
value={amount_credit ? Amounts.stringifyValue(amount_credit) : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Confirmation channel`}</label>
|
||||||
|
|
||||||
|
<div class="channel">
|
||||||
|
<input
|
||||||
|
class={
|
||||||
|
"pure-button content " +
|
||||||
|
(form.channel === TanChannel.EMAIL
|
||||||
|
? "pure-button-primary"
|
||||||
|
: "pure-button-secondary")
|
||||||
|
}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Email`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.channel = TanChannel.EMAIL;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class={
|
||||||
|
"pure-button content " +
|
||||||
|
(form.channel === TanChannel.SMS
|
||||||
|
? "pure-button-primary"
|
||||||
|
: "pure-button-secondary")
|
||||||
|
}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`SMS`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.channel = TanChannel.SMS;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class={
|
||||||
|
"pure-button content " +
|
||||||
|
(form.channel === TanChannel.FILE
|
||||||
|
? "pure-button-primary"
|
||||||
|
: "pure-button-secondary")
|
||||||
|
}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`FILE`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.channel = TanChannel.FILE;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.channel}
|
||||||
|
isDirty={form.channel !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<br />
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<button
|
||||||
|
class="pure-button pure-button-secondary btn-cancel"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.str`Cancel`}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="pure-button pure-button-primary btn-register"
|
||||||
|
type="submit"
|
||||||
|
disabled={!!errors}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (errors) return;
|
||||||
|
try {
|
||||||
|
const res = await createCashout({
|
||||||
|
amount_credit: Amounts.stringify(amount_credit),
|
||||||
|
amount_debit: Amounts.stringify(amount_debit),
|
||||||
|
subject: form.subject,
|
||||||
|
tan_channel: form.channel,
|
||||||
|
});
|
||||||
|
onComplete(res.data.uuid);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
const errorData: SandboxBackend.SandboxError =
|
||||||
|
error.info.error;
|
||||||
|
if (error.info.status === HttpStatusCode.PreconditionFailed) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`The account does not have sufficient funds`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
error.info.status === HttpStatusCode.ServiceUnavailable
|
||||||
|
) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`The bank does not support the TAN channel for this operation`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
} else if (error.info.status === HttpStatusCode.Conflict) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`No contact information for this channel`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`New cashout gave response error`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Cashout failed, please report`,
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.str`Create`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShowCashoutProps {
|
||||||
|
id: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
|
||||||
|
}
|
||||||
|
function ShowCashout({ id, onCancel, onLoadNotOk }: ShowCashoutProps): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useCashoutDetails(id);
|
||||||
|
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
|
||||||
|
const [code, setCode] = useState<string | undefined>(undefined);
|
||||||
|
const [error, saveError] = useState<ErrorMessage | undefined>();
|
||||||
|
if (!result.ok) return onLoadNotOk(result);
|
||||||
|
const errors = undefinedIfEmpty({
|
||||||
|
code: !code ? i18n.str`required` : undefined,
|
||||||
|
});
|
||||||
|
const isPending = String(result.data.status).toUpperCase() === "PENDING";
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Cashout details {id}</h1>
|
||||||
|
{error && (
|
||||||
|
<ErrorBanner error={error} onClear={() => saveError(undefined)} />
|
||||||
|
)}
|
||||||
|
<form class="pure-form">
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Subject</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.subject} />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Created</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.creation_time ?? ""} />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Confirmed</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.confirmation_time ?? ""} />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Debited</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.amount_debit} />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Credit</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.amount_credit} />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Status</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input readOnly value={result.data.status} />
|
||||||
|
</fieldset>
|
||||||
|
{isPending ? (
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<i18n.Translate>Code</i18n.Translate>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={code ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCode(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.code}
|
||||||
|
isDirty={code !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
) : undefined}
|
||||||
|
</form>
|
||||||
|
<br />
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<button
|
||||||
|
class="pure-button pure-button-secondary btn-cancel"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.str`Back`}
|
||||||
|
</button>
|
||||||
|
{isPending ? (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="pure-button pure-button-primary button-error"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const rest = await abortCashout(id);
|
||||||
|
onCancel();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
const errorData: SandboxBackend.SandboxError =
|
||||||
|
error.info.error;
|
||||||
|
if (
|
||||||
|
error.info.status === HttpStatusCode.PreconditionFailed
|
||||||
|
) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Cashout was already aborted`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Aborting cashout gave response error`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Aborting failed, please report`,
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.str`Abort`}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!code}
|
||||||
|
class="pure-button pure-button-primary "
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (!code) return;
|
||||||
|
const rest = await confirmCashout(id, {
|
||||||
|
tan: code,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
const errorData: SandboxBackend.SandboxError =
|
||||||
|
error.info.error;
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Confirmation of cashout gave response error`,
|
||||||
|
description: errorData.error.description,
|
||||||
|
debug: JSON.stringify(error.info),
|
||||||
|
});
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
saveError({
|
||||||
|
title: i18n.str`Confirmation failed, please report`,
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.str`Confirm`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -50,7 +50,6 @@ export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveErrorAndLogout(error: PageStateType["error"]): void {
|
function saveErrorAndLogout(error: PageStateType["error"]): void {
|
||||||
console.log("rrot", error);
|
|
||||||
saveError(error);
|
saveError(error);
|
||||||
backend.logOut();
|
backend.logOut();
|
||||||
}
|
}
|
||||||
@ -124,7 +123,6 @@ function handleNotOkResult(
|
|||||||
return function handleNotOkResult2<T, E>(
|
return function handleNotOkResult2<T, E>(
|
||||||
result: HttpResponsePaginated<T, E>,
|
result: HttpResponsePaginated<T, E>,
|
||||||
): VNode {
|
): VNode {
|
||||||
console.log("qweqwe", JSON.stringify(result, undefined, 2));
|
|
||||||
if (result.clientError && result.isUnauthorized) {
|
if (result.clientError && result.isUnauthorized) {
|
||||||
onErrorHandler({
|
onErrorHandler({
|
||||||
title: i18n.str`Wrong credentials for "${account}"`,
|
title: i18n.str`Wrong credentials for "${account}"`,
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/lib/index.browser";
|
} from "@gnu-taler/web-util/lib/index.browser";
|
||||||
import { createHashHistory } from "history";
|
import { createHashHistory } from "history";
|
||||||
import { h, VNode, } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { Router, route, Route } from "preact-router";
|
import { Router, route, Route } from "preact-router";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
|
@ -278,3 +278,35 @@ h1.nav {
|
|||||||
.pure-form > fieldset > input[disabled] {
|
.pure-form > fieldset > input[disabled] {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
|
.pure-form > fieldset > div > input[disabled] {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-form > fieldset > div.channel > div {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em;
|
||||||
|
border: 1px black solid;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.4em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-success {
|
||||||
|
background: rgb(28, 184, 65);
|
||||||
|
/* this is a green */
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-error {
|
||||||
|
background: rgb(202, 60, 60);
|
||||||
|
/* this is a maroon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-warning {
|
||||||
|
background: rgb(223, 117, 20);
|
||||||
|
/* this is an orange */
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background: rgb(66, 184, 221);
|
||||||
|
/* this is a light blue */
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@use "pure";
|
@use "pure";
|
||||||
@use "bank";
|
@use "bank";
|
||||||
@use "demo";
|
@use "demo";
|
||||||
|
@use "toggle";
|
||||||
@use "colors-bank";
|
@use "colors-bank";
|
||||||
|
51
packages/demobank-ui/src/scss/toggle.scss
Normal file
51
packages/demobank-ui/src/scss/toggle.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
$green: #56c080;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.toggle-switch {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 58px;
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: background 0.25s;
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
display: block;
|
||||||
|
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
transition: left 0.25s;
|
||||||
|
}
|
||||||
|
.toggle:hover &:before {
|
||||||
|
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.toggle-checkbox:checked + & {
|
||||||
|
background: $green;
|
||||||
|
&:before {
|
||||||
|
left: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.toggle-label {
|
||||||
|
margin-left: 5px;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
@ -59,6 +59,21 @@ export type WithIntermediate<Type extends object> = {
|
|||||||
: Type[prop] | undefined;
|
: Type[prop] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum TanChannel {
|
||||||
|
SMS = "sms",
|
||||||
|
EMAIL = "email",
|
||||||
|
FILE = "file",
|
||||||
|
}
|
||||||
|
export enum CashoutStatus {
|
||||||
|
// The payment was initiated after a valid
|
||||||
|
// TAN was received by the bank.
|
||||||
|
CONFIRMED = "confirmed",
|
||||||
|
|
||||||
|
// The cashout was created and now waits
|
||||||
|
// for the TAN by the author.
|
||||||
|
PENDING = "pending",
|
||||||
|
}
|
||||||
|
|
||||||
// export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> {
|
// export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> {
|
||||||
// const root = obj === undefined ? {} : obj;
|
// const root = obj === undefined ? {} : obj;
|
||||||
// return Object.entries(root).([key, value]) => {
|
// return Object.entries(root).([key, value]) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user