deposit design from belen, feature missing: kyc

This commit is contained in:
Sebastian 2022-01-10 16:04:53 -03:00
parent 83b9d32b78
commit fb22009ec4
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
35 changed files with 1545 additions and 1091 deletions

View File

@ -48,7 +48,7 @@ export const decorators = [
const isTestingHeader = (/.*\/header\/?.*/.test(kind));
if (isTestingHeader) {
// simple box with correct width and height
return <div style={{ width: 400, height: 320 }}>
return <div style={{ width: "fit-content" }}>
<Story />
</div>
}
@ -90,7 +90,7 @@ export const decorators = [
font-family: Arial, Helvetica, sans-serif;
}`}
</style>
<div style={{ width: 400, border: 'black solid 1px' }}>
<div style={{ border: 'black solid 1px', width: "fit-content" }}>
<Body />
</div>
</div>

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash
# This file is in the public domain.
set -e
[ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 1; }
[ "also-util" == "$1" ] && { pnpm -C ../taler-util/ prepare || exit 1; }
pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh

View File

@ -28,18 +28,18 @@ import { i18n } from "@gnu-taler/taler-util";
import { ComponentChildren, h, VNode } from "preact";
import Match from "preact-router/match";
import { PopupNavigation } from "./components/styled";
import { useDevContext } from "./context/devContext";
export enum Pages {
welcome = "/welcome",
balance = "/balance",
manual_withdraw = "/manual-withdraw",
balance_history = "/balance/history/:currency",
manual_withdraw = "/manual-withdraw/:currency?",
deposit = "/deposit/:currency",
settings = "/settings",
dev = "/dev",
cta = "/cta/:action",
backup = "/backup",
history = "/history",
last_activity = "/last-activity",
transaction = "/transaction/:tid",
provider_detail = "/provider/:pid",
provider_add = "/provider/add",
@ -78,7 +78,10 @@ export function NavBar({ devMode, path }: { path: string; devMode: boolean }) {
<PopupNavigation devMode={devMode}>
<div>
<Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
<Tab target="/history" current={path}>{i18n.str`History`}</Tab>
<Tab
target="/last-activity"
current={path}
>{i18n.str`Last Activity`}</Tab>
<Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab>
<Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab>
{devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>}
@ -87,8 +90,8 @@ export function NavBar({ devMode, path }: { path: string; devMode: boolean }) {
);
}
export function WalletNavBar() {
const { devMode } = useDevContext();
export function WalletNavBar({ devMode }: { devMode: boolean }) {
// const { devMode } = useDevContext();
return (
<Match>
{({ path }: any) => {

View File

@ -14,31 +14,28 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util";
import { Amounts, amountToPretty, Balance } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import {
ButtonPrimary,
TableWithRoundRows as TableWithRoundedRows,
} from "./styled";
import { TableWithRoundRows as TableWithRoundedRows } from "./styled";
export function BalanceTable({
balances,
goToWalletDeposit,
goToWalletHistory,
}: {
balances: Balance[];
goToWalletDeposit: (currency: string) => void;
goToWalletHistory: (currency: string) => void;
}): VNode {
const currencyFormatter = new Intl.NumberFormat("en-US");
return (
<TableWithRoundedRows>
{balances.map((entry, idx) => {
const av = Amounts.parseOrThrow(entry.available);
const v = currencyFormatter.format(
av.value + av.fraction / amountFractionalBase,
);
return (
<tr key={idx}>
<tr
key={idx}
onClick={() => goToWalletHistory(av.currency)}
style={{ cursor: "pointer" }}
>
<td>{av.currency}</td>
<td
style={{
@ -47,12 +44,7 @@ export function BalanceTable({
width: "100%",
}}
>
{v}
</td>
<td>
<ButtonPrimary onClick={() => goToWalletDeposit(av.currency)}>
Deposit
</ButtonPrimary>
{Amounts.stringifyValue(av)}
</td>
</tr>
);

View File

@ -0,0 +1,20 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h, VNode } from "preact";
export function Loading(): VNode {
return <div>Loading...</div>;
}

View File

@ -0,0 +1,95 @@
import { h, VNode } from "preact";
import arrowDown from "../../static/img/chevron-down.svg";
import { ButtonBoxPrimary, ButtonPrimary, ParagraphClickable } from "./styled";
import { useState } from "preact/hooks";
export interface Props {
label: (s: string) => string;
actions: string[];
onClick: (s: string) => void;
}
/**
* functionality: it will receive a list of actions, take the first actions as
* the first chosen action
* the user may change the chosen action
* when the user click the button it will call onClick with the chosen action
* as argument
*
* visually: it is a primary button with a select handler on the right
*
* @returns
*/
export function MultiActionButton({
label,
actions,
onClick: doClick,
}: Props): VNode {
const defaultAction = actions.length > 0 ? actions[0] : "";
const [opened, setOpened] = useState(false);
const [selected, setSelected] = useState<string>(defaultAction);
const canChange = actions.length > 1;
const options = canChange ? actions.filter((a) => a !== selected) : [];
function select(m: string): void {
setSelected(m);
setOpened(false);
}
if (!canChange) {
return (
<ButtonPrimary onClick={() => doClick(selected)}>
{label(selected)}
</ButtonPrimary>
);
}
return (
<div style={{ position: "relative", display: "inline-block" }}>
{opened && (
<div
style={{
position: "absolute",
bottom: 32 + 5,
right: 0,
marginLeft: 8,
marginRight: 8,
borderRadius: 5,
border: "1px solid blue",
background: "white",
boxShadow: "0px 8px 16px 0px rgba(0,0,0,0.2)",
zIndex: 1,
}}
>
{options.map((m) => (
<ParagraphClickable key={m} onClick={() => select(m)}>
{label(m)}
</ParagraphClickable>
))}
</div>
)}
<ButtonPrimary
onClick={() => doClick(selected)}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
marginRight: 0,
}}
>
{label(selected)}
</ButtonPrimary>
<ButtonBoxPrimary
onClick={() => setOpened((s) => !s)}
style={{
marginLeft: 0,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<img style={{ height: 14 }} src={arrowDown} />
</ButtonBoxPrimary>
</div>
);
}

View File

@ -43,7 +43,7 @@ export const WalletAction = styled.div`
}
section {
margin-bottom: 2em;
& button {
button {
margin-right: 8px;
margin-left: 8px;
}
@ -92,6 +92,10 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
border-bottom: 1px solid black;
border-top: 1px solid black;
}
button {
margin-right: 8px;
margin-left: 8px;
}
}
& > header {
@ -123,7 +127,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
justify-content: space-between;
display: flex;
background-color: #f7f7f7;
& button {
button {
margin-right: 8px;
margin-left: 8px;
}
@ -136,9 +140,9 @@ export const Middle = styled.div`
height: 100%;
`;
export const PopupBox = styled.div<{ noPadding?: boolean }>`
export const PopupBox = styled.div<{ noPadding?: boolean; devMode: boolean }>`
height: 290px;
width: 400px;
width: ${({ devMode }) => (!devMode ? "400px" : "500px")};
display: flex;
flex-direction: column;
justify-content: space-between;
@ -156,6 +160,10 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
border-bottom: 1px solid black;
border-top: 1px solid black;
}
button {
margin-right: 8px;
margin-left: 8px;
}
}
& > section[data-expanded] {
@ -196,7 +204,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
flex-direction: row;
justify-content: space-between;
display: flex;
& button {
button {
margin-right: 8px;
margin-left: 8px;
}
@ -363,11 +371,11 @@ export const CenteredDialog = styled.div`
export const Button = styled.button<{ upperCased?: boolean }>`
display: inline-block;
zoom: 1;
/* zoom: 1; */
line-height: normal;
white-space: nowrap;
vertical-align: middle;
text-align: center;
vertical-align: middle; //check this
/* text-align: center; */
cursor: pointer;
user-select: none;
box-sizing: border-box;
@ -379,7 +387,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
/* color: #444; rgba not supported (IE 8) */
color: rgba(0, 0, 0, 0.8); /* rgba supported */
border: 1px solid #999; /*IE 6/7/8*/
border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
/* border: none rgba(0, 0, 0, 0); IE9 + everything else */
background-color: "#e6e6e6";
text-decoration: none;
border-radius: 2px;
@ -401,11 +409,11 @@ export const Button = styled.button<{ upperCased?: boolean }>`
}
:hover {
filter: alpha(opacity=90);
filter: alpha(opacity=80);
background-image: linear-gradient(
transparent,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.1)
rgba(0, 0, 0, 0.1) 40%,
rgba(0, 0, 0, 0.2)
);
}
`;
@ -415,7 +423,7 @@ export const Link = styled.a<{ upperCased?: boolean }>`
zoom: 1;
line-height: normal;
white-space: nowrap;
vertical-align: middle;
/* vertical-align: middle; */
text-align: center;
cursor: pointer;
user-select: none;
@ -463,8 +471,8 @@ export const FontIcon = styled.div`
/* vertical-align: text-top; */
`;
export const ButtonBox = styled(Button)`
padding: 0.5em;
font-size: x-small;
padding: 8px;
/* font-size: small; */
& > ${FontIcon} {
width: 1em;
@ -472,12 +480,13 @@ export const ButtonBox = styled(Button)`
display: inline;
line-height: 0px;
}
background-color: transparent;
background-color: #f7f7f7;
border: 1px solid;
border-radius: 4px;
border-color: black;
color: black;
/* text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); */
/* -webkit-border-horizontal-spacing: 0px;
-webkit-border-vertical-spacing: 0px; */
`;
@ -499,6 +508,7 @@ export const LinkPrimary = styled(Link)`
export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
font-size: ${({ small }) => (small ? "small" : "inherit")};
background-color: rgb(66, 184, 221);
border-color: rgb(66, 184, 221);
`;
export const ButtonBoxPrimary = styled(ButtonBox)`
color: rgb(66, 184, 221);
@ -714,6 +724,7 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
border-top-right-radius: 0.25em;
border-color: ${({ invalid }) => (!invalid ? "lightgray" : "red")};
}
margin-bottom: 16px;
`;
export const ErrorText = styled.div`
@ -772,13 +783,13 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
display: flex;
& > div {
width: 400px;
width: ${({ devMode }) => (!devMode ? "400px" : "500px")};
}
& > div > a {
color: #f8faf7;
display: inline-block;
width: calc(400px / ${({ devMode }) => (!devMode ? 4 : 5)});
width: 100px;
text-align: center;
text-decoration: none;
vertical-align: middle;
@ -804,10 +815,9 @@ export const NiceSelect = styled.div`
box-shadow: none;
background-image: ${image};
background-position: right 0.5rem center;
background-position: right 8px center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
background-color: white;
@ -967,3 +977,17 @@ export const StyledCheckboxLabel = styled.div`
box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;
}
`;
export const ParagraphClickable = styled.p`
cursor: pointer;
margin: 0px;
padding: 8px 16px;
:hover {
filter: alpha(opacity=80);
background-image: linear-gradient(
transparent,
rgba(0, 0, 0, 0.1) 40%,
rgba(0, 0, 0, 0.2)
);
}
`;

View File

@ -42,5 +42,6 @@ export const DevContextProvider = ({ children }: { children: any }): VNode => {
const [value, setter] = useLocalStorage("devMode");
const devMode = value === "true";
const toggleDevMode = () => setter((v) => (!v ? "true" : undefined));
children = children.length === 1 && typeof children === "function" ? children({ devMode }) : children;
return h(Context.Provider, { value: { devMode, toggleDevMode }, children });
};

View File

@ -0,0 +1,168 @@
/*
This file is part of GNU Taler
(C) 2021 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 { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { PaymentRequestView as TestedComponent } from "./Deposit";
export default {
title: "cta/deposit",
component: TestedComponent,
argTypes: {},
};
export const NoBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
noncePriv: "",
proposalId: "proposal1234",
contractTerms: {
merchant: {
name: "someone",
},
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
amountRaw: "USD:10",
},
});
export const NoEnoughBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
noncePriv: "",
proposalId: "proposal1234",
contractTerms: {
merchant: {
name: "someone",
},
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
amountRaw: "USD:10",
},
balance: {
currency: "USD",
fraction: 40000000,
value: 9,
},
});
export const PaymentPossible = createExample(TestedComponent, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: "USD:10",
amountRaw: "USD:10",
noncePriv: "",
contractTerms: {
nonce: "123213123",
merchant: {
name: "someone",
},
amount: "USD:10",
summary: "some beers",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
},
});
export const PaymentPossibleWithFee = createExample(TestedComponent, {
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: "USD:10.20",
amountRaw: "USD:10",
noncePriv: "",
contractTerms: {
nonce: "123213123",
merchant: {
name: "someone",
},
amount: "USD:10",
summary: "some beers",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
},
});
export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: {
merchant: {
name: "someone",
},
fulfillment_message:
"congratulations! you are looking at the fulfillment message! ",
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: false,
},
});
export const AlreadyConfirmedWithoutFullfilment = createExample(
TestedComponent,
{
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: {
merchant: {
name: "someone",
},
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: false,
},
},
);
export const AlreadyPaid = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: {
merchant: {
name: "someone",
},
fulfillment_message:
"congratulations! you are looking at the fulfillment message! ",
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms> as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: true,
},
});

View File

@ -0,0 +1,251 @@
/*
This file is part of TALER
(C) 2015 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Page shown to the user to confirm entering
* a contract.
*/
/**
* Imports.
*/
// import * as i18n from "../i18n";
import {
AmountJson,
Amounts,
amountToPretty,
ConfirmPayResult,
ConfirmPayResultDone,
ConfirmPayResultType,
ContractTerms,
i18n,
NotificationType,
PreparePayResult,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorTalerOperation } from "../components/ErrorTalerOperation";
import { LogoHeader } from "../components/LogoHeader";
import { Part } from "../components/Part";
import {
ErrorBox,
SuccessBox,
WalletAction,
WarningBox,
} from "../components/styled";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import * as wxApi from "../wxApi";
interface Props {
talerPayUri?: string;
goBack: () => void;
}
export function DepositPage({ talerPayUri, goBack }: Props): VNode {
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(
undefined,
);
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
undefined,
);
const [payErrMsg, setPayErrMsg] = useState<
OperationFailedError | string | undefined
>(undefined);
const balance = useAsyncAsHook(wxApi.getBalance, [
NotificationType.CoinWithdrawn,
]);
const balanceWithoutError = balance?.hasError
? []
: balance?.response.balances || [];
const foundBalance = balanceWithoutError.find(
(b) =>
payStatus &&
Amounts.parseOrThrow(b.available).currency ===
Amounts.parseOrThrow(payStatus?.amountRaw).currency,
);
const foundAmount = foundBalance
? Amounts.parseOrThrow(foundBalance.available)
: undefined;
// We use a string here so that dependency tracking for useEffect works properly
const foundAmountStr = foundAmount
? Amounts.stringify(foundAmount)
: undefined;
useEffect(() => {
if (!talerPayUri) return;
const doFetch = async (): Promise<void> => {
try {
const p = await wxApi.preparePay(talerPayUri);
setPayStatus(p);
} catch (e) {
console.log("Got error while trying to pay", e);
if (e instanceof OperationFailedError) {
setPayErrMsg(e);
}
if (e instanceof Error) {
setPayErrMsg(e.message);
}
}
};
doFetch();
}, [talerPayUri, foundAmountStr]);
if (!talerPayUri) {
return <span>missing pay uri</span>;
}
if (!payStatus) {
if (payErrMsg instanceof OperationFailedError) {
return (
<WalletAction>
<LogoHeader />
<h2>{i18n.str`Digital cash payment`}</h2>
<section>
<ErrorTalerOperation
title="Could not get the payment information for this order"
error={payErrMsg?.operationError}
/>
</section>
</WalletAction>
);
}
if (payErrMsg) {
return (
<WalletAction>
<LogoHeader />
<h2>{i18n.str`Digital cash payment`}</h2>
<section>
<p>Could not get the payment information for this order</p>
<ErrorBox>{payErrMsg}</ErrorBox>
</section>
</WalletAction>
);
}
return <span>Loading payment information ...</span>;
}
const onClick = async (): Promise<void> => {
// try {
// const res = await doPayment(payStatus);
// setPayResult(res);
// } catch (e) {
// console.error(e);
// if (e instanceof Error) {
// setPayErrMsg(e.message);
// }
// }
};
return (
<PaymentRequestView
uri={talerPayUri}
payStatus={payStatus}
payResult={payResult}
onClick={onClick}
balance={foundAmount}
/>
);
}
export interface PaymentRequestViewProps {
payStatus: PreparePayResult;
payResult?: ConfirmPayResult;
onClick: () => void;
payErrMsg?: string;
uri: string;
balance: AmountJson | undefined;
}
export function PaymentRequestView({
uri,
payStatus,
payResult,
onClick,
balance,
}: PaymentRequestViewProps): VNode {
let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
const contractTerms: ContractTerms = payStatus.contractTerms;
return (
<WalletAction>
<LogoHeader />
<h2>{i18n.str`Digital cash deposit`}</h2>
{payStatus.status === PreparePayResultType.AlreadyConfirmed &&
(payStatus.paid ? (
<SuccessBox> Already paid </SuccessBox>
) : (
<WarningBox> Already claimed </WarningBox>
))}
{payResult && payResult.type === ConfirmPayResultType.Done && (
<SuccessBox>
<h3>Payment complete</h3>
<p>
{!payResult.contractTerms.fulfillment_message
? "You will now be sent back to the merchant you came from."
: payResult.contractTerms.fulfillment_message}
</p>
</SuccessBox>
)}
<section>
{payStatus.status !== PreparePayResultType.InsufficientBalance &&
Amounts.isNonZero(totalFees) && (
<Part
big
title="Total to pay"
text={amountToPretty(
Amounts.parseOrThrow(payStatus.amountEffective),
)}
kind="negative"
/>
)}
<Part
big
title="Purchase amount"
text={amountToPretty(Amounts.parseOrThrow(payStatus.amountRaw))}
kind="neutral"
/>
{Amounts.isNonZero(totalFees) && (
<Fragment>
<Part
big
title="Fee"
text={amountToPretty(totalFees)}
kind="negative"
/>
</Fragment>
)}
<Part
title="Merchant"
text={contractTerms.merchant.name}
kind="neutral"
/>
<Part title="Purchase" text={contractTerms.summary} kind="neutral" />
{contractTerms.order_id && (
<Part
title="Receipt"
text={`#${contractTerms.order_id}`}
kind="neutral"
/>
)}
</section>
</WalletAction>
);
}

View File

@ -57,35 +57,10 @@ import * as wxApi from "../wxApi";
interface Props {
talerPayUri?: string;
goToWalletManualWithdraw: () => void;
goToWalletManualWithdraw: (currency?: string) => void;
goBack: () => void;
}
// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) {
// const fulfillmentUrl = payStatus.contractTerms.fulfillment_url;
// let message;
// if (fulfillmentUrl) {
// message = (
// <span>
// You have already paid for this article. Click{" "}
// <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again.
// </span>
// );
// } else {
// message = <span>
// You have already paid for this article:{" "}
// <em>
// {payStatus.contractTerms.fulfillment_message ?? "no message given"}
// </em>
// </span>;
// }
// return <section class="main">
// <h1>GNU Taler Wallet</h1>
// <article class="fade">
// {message}
// </article>
// </section>
// }
const doPayment = async (
payStatus: PreparePayResult,
): Promise<ConfirmPayResultDone> => {

View File

@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, NullLink } from "../test-utils";
import { createExample } from "../test-utils";
import { BalanceView as TestedComponent } from "./BalancePage";
export default {
@ -28,211 +28,124 @@ export default {
argTypes: {},
};
export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
message: "Network error",
},
Linker: NullLink,
});
export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [],
},
},
Linker: NullLink,
balances: [],
});
export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
});
export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:5.11",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
});
export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:5.11",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
});
export const SomeCoinsAndMovingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:2",
pendingOutgoing: "USD:5.11",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
});
export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:5.1",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:3.01",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
],
});
export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:1",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "TESTKUDOS:2000",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
balances: [
{
available: "EUR:1",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
{
available: "TESTKUDOS:2000",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "JPY:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
});
export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
balances: [
{
available: "EUR:3",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "ARS:1",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
});
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:13451",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:202.02",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "ARS:30",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "DEMOKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "TESTKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
balances: [
{
available: "USD:0",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
{
available: "ARS:13451",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:202.02",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:0",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "DEMOKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "TESTKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
});

View File

@ -14,70 +14,81 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { BalancesResponse, i18n } from "@gnu-taler/taler-util";
import { Amounts, Balance, i18n } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { BalanceTable } from "../components/BalanceTable";
import { ButtonPrimary, ErrorBox } from "../components/styled";
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { PageLink } from "../renderHtml";
import * as wxApi from "../wxApi";
import { MultiActionButton } from "../components/MultiActionButton";
import { Loading } from "../components/Loading";
interface Props {
goToWalletDeposit: (currency: string) => void;
goToWalletHistory: (currency: string) => void;
goToWalletManualWithdraw: () => void;
}
export function BalancePage({
goToWalletManualWithdraw,
goToWalletDeposit,
goToWalletHistory,
}: Props): VNode {
const state = useAsyncAsHook(wxApi.getBalance);
return (
<BalanceView
balance={state}
Linker={PageLink}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
/>
);
}
export interface BalanceViewProps {
balance: HookResponse<BalancesResponse>;
Linker: typeof PageLink;
goToWalletManualWithdraw: () => void;
goToWalletDeposit: (currency: string) => void;
}
const balances = !state || state.hasError ? [] : state.response.balances;
export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
goToWalletDeposit,
}: BalanceViewProps): VNode {
if (!balance) {
return <div>Loading...</div>;
if (!state) {
return <Loading />;
}
if (balance.hasError) {
if (state.hasError) {
return (
<Fragment>
<ErrorBox>{balance.message}</ErrorBox>
<ErrorBox>{state.message}</ErrorBox>
<p>
Click <Linker pageName="welcome">here</Linker> for help and
Click <PageLink pageName="welcome">here</PageLink> for help and
diagnostics.
</p>
</Fragment>
);
}
if (balance.response.balances.length === 0) {
return (
<BalanceView
balances={balances}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
goToWalletHistory={goToWalletHistory}
/>
);
}
export interface BalanceViewProps {
balances: Balance[];
goToWalletManualWithdraw: () => void;
goToWalletDeposit: (currency: string) => void;
goToWalletHistory: (currency: string) => void;
}
export function BalanceView({
balances,
goToWalletManualWithdraw,
goToWalletDeposit,
goToWalletHistory,
}: BalanceViewProps): VNode {
const currencyWithNonZeroAmount = balances
.filter((b) => !Amounts.isZero(b.available))
.map((b) => b.available.split(":")[0]);
if (balances.length === 0) {
return (
<Fragment>
<p>
<i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
<PageLink pageName="/welcome">help</PageLink> getting started?
</i18n.Translate>
</p>
<footer style={{ justifyContent: "space-between" }}>
<div />
<ButtonPrimary onClick={goToWalletManualWithdraw}>
Withdraw
</ButtonPrimary>
@ -90,15 +101,21 @@ export function BalanceView({
<Fragment>
<section>
<BalanceTable
balances={balance.response.balances}
goToWalletDeposit={goToWalletDeposit}
balances={balances}
goToWalletHistory={goToWalletHistory}
/>
</section>
<footer style={{ justifyContent: "space-between" }}>
<div />
<ButtonPrimary onClick={goToWalletManualWithdraw}>
Withdraw
</ButtonPrimary>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
label={(s) => `Deposit ${s}`}
actions={currencyWithNonZeroAmount}
onClick={(c) => goToWalletDeposit(c)}
/>
)}
</footer>
</Fragment>
);

View File

@ -86,10 +86,6 @@ export function View({
return (
<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("/static/popup.html")}>
wallet tab
</button>
<button onClick={confirmReset}>reset</button>
<br />
<button onClick={onExportDatabase}>export database</button>
@ -109,7 +105,8 @@ export function View({
"yyyy/MM/dd_HH:mm",
)}.json`}
>
click here
{" "}
click here{" "}
</a>
to download
</div>

View File

@ -1,213 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021 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 {
PaymentStatus,
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { HistoryView as TestedComponent } from "./History";
export default {
title: "popup/history/list",
component: TestedComponent,
};
const commonTransaction = {
amountRaw: "USD:10",
amountEffective: "USD:9",
pending: false,
timestamp: {
t_ms: new Date().getTime(),
},
transactionId: "12",
} as TransactionCommon;
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
confirmed: false,
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
},
} as TransactionWithdrawal,
payment: {
...commonTransaction,
amountEffective: "USD:11",
type: TransactionType.Payment,
info: {
contractTermsHash: "ASDZXCASD",
merchant: {
name: "the merchant",
},
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "the summary",
fulfillmentMessage: "",
},
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
deposit: {
...commonTransaction,
type: TransactionType.Deposit,
depositGroupId: "#groupId",
targetPaytoUri: "payto://x-taler-bank/bank/account",
} as TransactionDeposit,
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
merchantBaseUrl: "http://merchant.taler",
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
contractTermsHash: "ASDZXCASD",
merchant: {
name: "the merchant",
},
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "the summary",
fulfillmentMessage: "",
},
} as TransactionRefund,
};
export const EmptyWithBalance = createExample(TestedComponent, {
list: [],
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const EmptyWithNoBalance = createExample(TestedComponent, {
list: [],
balances: [],
});
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const OnePending = createExample(TestedComponent, {
list: [
{
...exampleData.withdraw,
pending: true,
},
],
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const Several = createExample(TestedComponent, {
list: [
exampleData.withdraw,
exampleData.payment,
exampleData.withdraw,
exampleData.payment,
exampleData.refresh,
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
list: [
exampleData.withdraw,
exampleData.payment,
exampleData.withdraw,
exampleData.payment,
exampleData.refresh,
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});

View File

@ -1,148 +0,0 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
AmountString,
Balance,
i18n,
Transaction,
TransactionsResponse,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ButtonPrimary } from "../components/styled";
import { TransactionItem } from "../components/TransactionItem";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import * as wxApi from "../wxApi";
import { AddNewActionView } from "./AddNewActionView";
export function HistoryPage(): VNode {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
const balance = useAsyncAsHook(wxApi.getBalance);
const balanceWithoutError = balance?.hasError
? []
: balance?.response.balances || [];
useEffect(() => {
const fetchData = async (): Promise<void> => {
const res = await wxApi.getTransactions();
setTransactions(res);
};
fetchData();
}, []);
const [addingAction, setAddingAction] = useState(false);
if (addingAction) {
return <AddNewActionView onCancel={() => setAddingAction(false)} />;
}
if (!transactions) {
return <div>Loading ...</div>;
}
return (
<HistoryView
balances={balanceWithoutError}
list={[...transactions.transactions].reverse()}
onAddNewAction={() => setAddingAction(true)}
/>
);
}
function amountToString(c: AmountString): string {
const idx = c.indexOf(":");
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
}
export function HistoryView({
list,
balances,
onAddNewAction,
}: {
list: Transaction[];
balances: Balance[];
onAddNewAction: () => void;
}): VNode {
const multiCurrency = balances.length > 1;
return (
<Fragment>
<header>
{balances.length > 0 ? (
<Fragment>
{multiCurrency ? (
<div class="title">
Balance:{" "}
<ul style={{ margin: 0 }}>
{balances.map((b, i) => (
<li key={i}>{b.available}</li>
))}
</ul>
</div>
) : (
<div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>
)}
</Fragment>
) : (
<div />
)}
<div>
<ButtonPrimary onClick={onAddNewAction}>
<b>+</b>
</ButtonPrimary>
</div>
</header>
{list.length === 0 ? (
<section data-expanded data-centered>
<p>
<i18n.Translate>
You have no history yet, here you will be able to check your last
transactions.
</i18n.Translate>
</p>
</section>
) : (
<section>
{list.slice(0, 3).map((tx, i) => (
<TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} />
))}
</section>
)}
<footer style={{ justifyContent: "space-around" }}>
{list.length > 0 && (
<a
target="_blank"
rel="noopener noreferrer"
style={{ color: "darkgreen", textDecoration: "none" }}
href={
// eslint-disable-next-line no-undef
typeof chrome !== "undefined" && chrome.extension
? // eslint-disable-next-line no-undef
chrome.extension.getURL(`/static/wallet.html#/history`)
: "#"
}
>
VIEW MORE TRANSACTIONS
</a>
)}
</footer>
</Fragment>
);
}

View File

@ -19,11 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import * as a1 from "./AddNewActionView.stories";
import * as a1 from "../wallet/AddNewActionView.stories";
import * as a2 from "./Balance.stories";
import * as a3 from "./DeveloperPage.stories";
import * as a4 from "./History.stories";
import * as a5 from "./Popup.stories";
import * as a6 from "./TalerActionFound.stories";
export default [a1, a2, a3, a4, a5, a6];
export default [a1, a2, a3, a5, a6];

View File

@ -33,13 +33,13 @@ import { Pages, WalletNavBar } from "./NavigationBar";
import { BackupPage } from "./wallet/BackupPage";
import { BalancePage } from "./popup/BalancePage";
import { DeveloperPage } from "./popup/DeveloperPage";
import { HistoryPage } from "./popup/History";
import { ProviderAddPage } from "./wallet/ProviderAddPage";
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
import { SettingsPage } from "./popup/Settings";
import { TalerActionFound } from "./popup/TalerActionFound";
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
import { IoCProviderForRuntime } from "./context/iocContext";
import { LastActivityPage } from "./wallet/LastActivityPage";
function main(): void {
try {
@ -77,12 +77,13 @@ function CheckTalerActionComponent(): VNode {
function Application() {
return (
<div>
<DevContextProvider>
// <div>
<DevContextProvider>
{({ devMode }: { devMode: boolean }) => (
<IoCProviderForRuntime>
<WalletNavBar />
<WalletNavBar devMode={devMode} />
<CheckTalerActionComponent />
<PopupBox>
<PopupBox devMode={devMode}>
<Router history={createHashHistory()}>
<Route path={Pages.dev} component={DeveloperPage} />
@ -90,10 +91,14 @@ function Application() {
path={Pages.balance}
component={BalancePage}
goToWalletManualWithdraw={() =>
goToWalletPage(Pages.manual_withdraw)
goToWalletPage(
Pages.manual_withdraw.replace(":currency?", ""),
)
}
goToWalletDeposit={(currency: string) =>
goToWalletPage(Pages.deposit.replace(":currency", currency))
goToWalletHistory={(currency: string) =>
goToWalletPage(
Pages.balance_history.replace(":currency", currency),
)
}
/>
<Route path={Pages.settings} component={SettingsPage} />
@ -114,6 +119,8 @@ function Application() {
}}
/>
<Route path={Pages.last_activity} component={LastActivityPage} />
<Route
path={Pages.transaction}
component={({ tid }: { tid: string }) =>
@ -121,8 +128,6 @@ function Application() {
}
/>
<Route path={Pages.history} component={HistoryPage} />
<Route
path={Pages.backup}
component={BackupPage}
@ -157,8 +162,9 @@ function Application() {
</Router>
</PopupBox>
</IoCProviderForRuntime>
</DevContextProvider>
</div>
)}
</DevContextProvider>
// </div>
);
}

View File

@ -162,7 +162,12 @@ export function PageLink(props: {
children?: ComponentChildren;
}): VNode {
// eslint-disable-next-line no-undef
const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);
const url =
typeof chrome === "undefined"
? undefined
: // eslint-disable-next-line no-undef
chrome.extension?.getURL(`/static/wallet.html#/${props.pageName}`);
return (
<a class="actionLink" href={url} target="_blank" rel="noopener noreferrer">
{props.children}

View File

@ -117,5 +117,6 @@ export function mountBrowser<T>(callback: () => T, Context?: ({ children }: { ch
}
}
const nullTestFunction = {} as TestFunction
export const justBrowser_it: PendingTestFunction | TestFunction =
typeof window === 'undefined' ? it.skip : it
typeof it === 'undefined' ? nullTestFunction : (typeof window === 'undefined' ? it.skip : it)

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils";
import { AddNewActionView as TestedComponent } from "./AddNewActionView";
export default {
title: "popup/add new action",
title: "wallet/add new action",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),

View File

@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, NullLink } from "../test-utils";
import { createExample } from "../test-utils";
import { BalanceView as TestedComponent } from "./BalancePage";
export default {
@ -28,83 +28,124 @@ export default {
argTypes: {},
};
export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
message: "Network error",
},
Linker: NullLink,
});
export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [],
},
},
Linker: NullLink,
balances: [],
});
export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
],
});
export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:5.11",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
balances: [
{
available: "EUR:1",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
{
available: "TESTKUDOS:2000",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "JPY:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
});
export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:5",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
balances: [
{
available: "EUR:3",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
},
Linker: NullLink,
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "ARS:1",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
});
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
balances: [
{
available: "USD:0",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "ARS:13451",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:202.02",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:0",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "DEMOKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "TESTKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
});

View File

@ -14,68 +14,87 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { BalancesResponse, i18n } from "@gnu-taler/taler-util";
import { Amounts, Balance, i18n } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { BalanceTable } from "../components/BalanceTable";
import { ButtonPrimary, Centered, ErrorBox } from "../components/styled";
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { Loading } from "../components/Loading";
import { MultiActionButton } from "../components/MultiActionButton";
import {
ButtonPrimary,
Centered,
ErrorBox,
SuccessBox,
} from "../components/styled";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { PageLink } from "../renderHtml";
import * as wxApi from "../wxApi";
interface Props {
goToWalletDeposit: (currency: string) => void;
goToWalletHistory: (currency: string) => void;
goToWalletManualWithdraw: () => void;
}
export function BalancePage({
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
goToWalletDeposit: (currency: string) => void;
goToWalletManualWithdraw: () => void;
}): VNode {
goToWalletHistory,
}: Props): VNode {
const state = useAsyncAsHook(wxApi.getBalance);
return (
<BalanceView
balance={state}
Linker={PageLink}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
/>
);
}
export interface BalanceViewProps {
balance: HookResponse<BalancesResponse>;
Linker: typeof PageLink;
goToWalletManualWithdraw: () => void;
goToWalletDeposit: (currency: string) => void;
}
const balances = !state || state.hasError ? [] : state.response.balances;
export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
goToWalletDeposit,
}: BalanceViewProps): VNode {
if (!balance) {
return <div>Loading...</div>;
if (!state) {
return <Loading />;
}
if (balance.hasError) {
if (state.hasError) {
return (
<Fragment>
<ErrorBox>{balance.message}</ErrorBox>
<ErrorBox>{state.message}</ErrorBox>
<p>
Click <Linker pageName="welcome">here</Linker> for help and
Click <PageLink pageName="welcome">here</PageLink> for help and
diagnostics.
</p>
</Fragment>
);
}
if (balance.response.balances.length === 0) {
return (
<BalanceView
balances={balances}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
goToWalletHistory={goToWalletHistory}
/>
);
}
export interface BalanceViewProps {
balances: Balance[];
goToWalletManualWithdraw: () => void;
goToWalletDeposit: (currency: string) => void;
goToWalletHistory: (currency: string) => void;
}
export function BalanceView({
balances,
goToWalletManualWithdraw,
goToWalletDeposit,
goToWalletHistory,
}: BalanceViewProps): VNode {
const currencyWithNonZeroAmount = balances
.filter((b) => !Amounts.isZero(b.available))
.map((b) => b.available.split(":")[0]);
if (balances.length === 0) {
return (
<Fragment>
<p>
<Centered style={{ marginTop: 100 }}>
<i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
<PageLink pageName="/welcome">help</PageLink> getting started?
</i18n.Translate>
</Centered>
</p>
@ -93,15 +112,21 @@ export function BalanceView({
<Fragment>
<section>
<BalanceTable
balances={balance.response.balances}
goToWalletDeposit={goToWalletDeposit}
balances={balances}
goToWalletHistory={goToWalletHistory}
/>
</section>
<footer style={{ justifyContent: "space-between" }}>
<div />
<ButtonPrimary onClick={goToWalletManualWithdraw}>
Withdraw
</ButtonPrimary>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
label={(s) => `Deposit ${s}`}
actions={currencyWithNonZeroAmount}
onClick={(c) => goToWalletDeposit(c)}
/>
)}
</footer>
</Fragment>
);

View File

@ -41,12 +41,14 @@ export interface Props {
exchangeList: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
onAddExchange: () => void;
initialCurrency?: string;
}
export function CreateManualWithdraw({
initialAmount,
exchangeList,
error,
initialCurrency,
onCreate,
onAddExchange,
}: Props): VNode {
@ -61,8 +63,16 @@ export function CreateManualWithdraw({
{} as Record<string, string>,
);
const foundExchangeForCurrency = exchangeSelectList.findIndex(
(e) => exchangeList[e] === initialCurrency,
);
const initialExchange =
exchangeSelectList.length > 0 ? exchangeSelectList[0] : "";
foundExchangeForCurrency !== -1
? exchangeSelectList[foundExchangeForCurrency]
: exchangeSelectList.length > 0
? exchangeSelectList[0]
: "";
const [exchange, setExchange] = useState(initialExchange || "");
const [currency, setCurrency] = useState(exchangeList[initialExchange] ?? "");

View File

@ -23,23 +23,24 @@ import {
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Part } from "../components/Part";
import { Loading } from "../components/Loading";
import { SelectList } from "../components/SelectList";
import {
ButtonBoxWarning,
ButtonPrimary,
ErrorText,
Input,
InputWithLabel,
WarningBox,
} from "../components/styled";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import * as wxApi from "../wxApi";
interface Props {
currency: string;
onSuccess: (currency: string) => void;
}
export function DepositPage({ currency }: Props): VNode {
const [success, setSuccess] = useState(false);
export function DepositPage({ currency, onSuccess }: Props): VNode {
const state = useAsyncAsHook(async () => {
const balance = await wxApi.getBalance();
const bs = balance.balances.filter((b) => b.available.startsWith(currency));
@ -63,7 +64,7 @@ export function DepositPage({ currency }: Props): VNode {
async function doSend(account: string, amount: AmountString): Promise<void> {
await wxApi.createDepositGroup(account, amount);
setSuccess(true);
onSuccess(currency);
}
async function getFeeForAmount(
@ -73,8 +74,8 @@ export function DepositPage({ currency }: Props): VNode {
return await wxApi.getFeeForDeposit(account, amount);
}
if (accounts.length === 0) return <div>loading..</div>;
if (success) return <div>deposit created</div>;
if (accounts.length === 0) return <Loading />;
return (
<View
knownBankAccounts={accounts}
@ -105,8 +106,17 @@ export function View({
const [accountIdx, setAccountIdx] = useState(0);
const [amount, setAmount] = useState<number | undefined>(undefined);
const [fee, setFee] = useState<DepositFee | undefined>(undefined);
function updateAmount(num: number | undefined) {
setAmount(num);
setFee(undefined);
}
const feeHasBeenCalculated = fee !== undefined;
const currency = balance.currency;
const amountStr: AmountString = `${currency}:${amount}`;
const feeSum =
fee !== undefined
? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount
: Amounts.getZero(currency);
const account = knownBankAccounts.length
? knownBankAccounts[accountIdx]
@ -126,7 +136,12 @@ export function View({
return <div>no balance</div>;
}
if (!knownBankAccounts || !knownBankAccounts.length) {
return <div>there is no known bank account to send money to</div>;
return (
<WarningBox>
<p>There is no known bank account to send money to</p>
<ButtonBoxWarning>Withdraw</ButtonBoxWarning>
</WarningBox>
);
}
const parsedAmount =
amount === undefined ? undefined : Amounts.parse(amountStr);
@ -136,9 +151,16 @@ export function View({
: !parsedAmount
? "Invalid amount"
: Amounts.cmp(balance, parsedAmount) === -1
? `To much, your current balance is ${balance.value}`
? `To much, your current balance is ${Amounts.stringifyValue(balance)}`
: undefined;
const totalToDeposit = parsedAmount
? Amounts.sub(parsedAmount, feeSum).amount
: Amounts.getZero(currency);
const unableToDeposit =
Amounts.isZero(totalToDeposit) && feeHasBeenCalculated;
return (
<Fragment>
<h2>Send {currency} to your account</h2>
@ -153,7 +175,7 @@ export function View({
/>
</Input>
<InputWithLabel invalid={!!error}>
<label>Amount to send</label>
<label>Amount</label>
<div>
<span>{currency}</span>
<input
@ -161,11 +183,10 @@ export function View({
value={amount}
onInput={(e) => {
const num = parseFloat(e.currentTarget.value);
console.log(num);
if (!Number.isNaN(num)) {
setAmount(num);
updateAmount(num);
} else {
setAmount(undefined);
updateAmount(undefined);
setFee(undefined);
}
}}
@ -173,40 +194,41 @@ export function View({
</div>
{error && <ErrorText>{error}</ErrorText>}
</InputWithLabel>
{!error && fee && (
<div style={{ textAlign: "center" }}>
<Part
title="Exchange fee"
text={Amounts.stringify(Amounts.sum([fee.wire, fee.coin]).amount)}
kind="negative"
/>
<Part
title="Change cost"
text={Amounts.stringify(fee.refresh)}
kind="negative"
/>
{parsedAmount && (
<Part
title="Total received"
text={Amounts.stringify(
Amounts.sub(
parsedAmount,
Amounts.sum([fee.wire, fee.coin]).amount,
).amount,
)}
kind="positive"
/>
)}
</div>
)}
{
<Fragment>
<InputWithLabel>
<label>Deposit fee</label>
<div>
<span>{currency}</span>
<input
type="number"
disabled
value={Amounts.stringifyValue(feeSum)}
/>
</div>
</InputWithLabel>
<InputWithLabel>
<label>Total deposit</label>
<div>
<span>{currency}</span>
<input
type="number"
disabled
value={Amounts.stringifyValue(totalToDeposit)}
/>
</div>
</InputWithLabel>
</Fragment>
}
</section>
<footer>
<div />
<ButtonPrimary
disabled={!parsedAmount}
disabled={unableToDeposit}
onClick={() => onSend(accountURI, amountStr)}
>
Send
Deposit {Amounts.stringifyValue(totalToDeposit)} {currency}
</ButtonPrimary>
</footer>
</Fragment>

View File

@ -35,7 +35,7 @@ import { HistoryView as TestedComponent } from "./History";
import { createExample } from "../test-utils";
export default {
title: "wallet/history/list",
title: "wallet/balance/history",
component: TestedComponent,
};
@ -114,8 +114,13 @@ const exampleData = {
} as TransactionRefund,
};
export const Empty = createExample(TestedComponent, {
list: [],
export const NoBalance = createExample(TestedComponent, {
transactions: [],
balances: [],
});
export const SomeBalanceWithNoTransactions = createExample(TestedComponent, {
transactions: [],
balances: [
{
available: "TESTKUDOS:10",
@ -127,13 +132,8 @@ export const Empty = createExample(TestedComponent, {
],
});
export const EmptyWithNoBalance = createExample(TestedComponent, {
list: [],
balances: [],
});
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
export const OneSimpleTransaction = createExample(TestedComponent, {
transactions: [exampleData.withdraw],
balances: [
{
available: "USD:10",
@ -145,8 +145,21 @@ export const One = createExample(TestedComponent, {
],
});
export const OnePending = createExample(TestedComponent, {
list: [
export const TwoTransactionsAndZeroBalance = createExample(TestedComponent, {
transactions: [exampleData.withdraw, exampleData.deposit],
balances: [
{
available: "USD:0",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const OneTransactionPending = createExample(TestedComponent, {
transactions: [
{
...exampleData.withdraw,
pending: true,
@ -163,8 +176,8 @@ export const OnePending = createExample(TestedComponent, {
],
});
export const Several = createExample(TestedComponent, {
list: [
export const SomeTransactions = createExample(TestedComponent, {
transactions: [
exampleData.withdraw,
exampleData.payment,
exampleData.withdraw,
@ -182,35 +195,6 @@ export const Several = createExample(TestedComponent, {
exampleData.deposit,
],
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
list: [
exampleData.withdraw,
exampleData.payment,
exampleData.withdraw,
exampleData.payment,
exampleData.refresh,
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "USD:10",
pendingIncoming: "USD:0",
@ -220,3 +204,76 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
},
],
});
export const SomeTransactionsWithTwoCurrencies = createExample(
TestedComponent,
{
transactions: [
exampleData.withdraw,
exampleData.payment,
exampleData.withdraw,
exampleData.payment,
exampleData.refresh,
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
balances: [
{
available: "USD:0",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
},
);
export const FiveOfficialCurrencies = createExample(TestedComponent, {
transactions: [exampleData.withdraw],
balances: [
{
available: "USD:1000",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "EUR:881",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "COL:4043000.5",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "JPY:11564450.6",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "GBP:736",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});

View File

@ -15,21 +15,38 @@
*/
import {
AmountString,
Amounts,
Balance,
NotificationType,
Transaction,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ButtonPrimary, DateSeparator } from "../components/styled";
import { Loading } from "../components/Loading";
import {
ButtonBoxPrimary,
ButtonBoxWarning,
ButtonPrimary,
DateSeparator,
ErrorBox,
NiceSelect,
WarningBox,
} from "../components/styled";
import { Time } from "../components/Time";
import { TransactionItem } from "../components/TransactionItem";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { AddNewActionView } from "../popup/AddNewActionView";
import * as wxApi from "../wxApi";
export function HistoryPage(): VNode {
interface Props {
currency?: string;
goToWalletDeposit: (currency: string) => void;
goToWalletManualWithdraw: (currency?: string) => void;
}
export function HistoryPage({
currency,
goToWalletManualWithdraw,
goToWalletDeposit,
}: Props): VNode {
const balance = useAsyncAsHook(wxApi.getBalance);
const balanceWithoutError = balance?.hasError
? []
@ -39,110 +56,166 @@ export function HistoryPage(): VNode {
NotificationType.WithdrawGroupFinished,
]);
const [addingAction, setAddingAction] = useState(false);
if (addingAction) {
return <AddNewActionView onCancel={() => setAddingAction(false)} />;
if (!transactionQuery || !balance) {
return <Loading />;
}
if (!transactionQuery) {
return <div>Loading ...</div>;
}
if (transactionQuery.hasError) {
return <div>There was an error loading the transactions.</div>;
return (
<Fragment>
<ErrorBox>{transactionQuery.message}</ErrorBox>
<p>There was an error loading the transactions.</p>
</Fragment>
);
}
return (
<HistoryView
balances={balanceWithoutError}
list={[...transactionQuery.response.transactions].reverse()}
onAddNewAction={() => setAddingAction(true)}
defaultCurrency={currency}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
transactions={[...transactionQuery.response.transactions].reverse()}
/>
);
}
function amountToString(c: AmountString): string {
const idx = c.indexOf(":");
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
}
const term = 1000 * 60 * 60 * 24;
function normalizeToDay(x: number): number {
return Math.round(x / term) * term;
}
export function HistoryView({
list,
defaultCurrency,
transactions,
balances,
onAddNewAction,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
list: Transaction[];
goToWalletDeposit: (currency: string) => void;
goToWalletManualWithdraw: (currency?: string) => void;
defaultCurrency?: string;
transactions: Transaction[];
balances: Balance[];
onAddNewAction: () => void;
}): VNode {
const byDate = list.reduce((rv, x) => {
const theDate =
x.timestamp.t_ms === "never" ? 0 : normalizeToDay(x.timestamp.t_ms);
if (theDate) {
(rv[theDate] = rv[theDate] || []).push(x);
}
const currencies = balances.map((b) => b.available.split(":")[0]);
return rv;
}, {} as { [x: string]: Transaction[] });
const defaultCurrencyIndex = currencies.findIndex(
(c) => c === defaultCurrency,
);
const [currencyIndex, setCurrencyIndex] = useState(
defaultCurrencyIndex === -1 ? 0 : defaultCurrencyIndex,
);
const selectedCurrency =
currencies.length > 0 ? currencies[currencyIndex] : undefined;
const currencyAmount = balances[currencyIndex]
? Amounts.jsonifyAmount(balances[currencyIndex].available)
: undefined;
const byDate = transactions
.filter((t) => t.amountRaw.split(":")[0] === selectedCurrency)
.reduce((rv, x) => {
const theDate =
x.timestamp.t_ms === "never" ? 0 : normalizeToDay(x.timestamp.t_ms);
if (theDate) {
(rv[theDate] = rv[theDate] || []).push(x);
}
return rv;
}, {} as { [x: string]: Transaction[] });
const datesWithTransaction = Object.keys(byDate);
const multiCurrency = balances.length > 1;
if (balances.length === 0 || !selectedCurrency) {
return (
<WarningBox>
<p>
You have <b>no balance</b>. Withdraw some founds into your wallet
</p>
<ButtonBoxWarning onClick={() => goToWalletManualWithdraw()}>
Withdraw
</ButtonBoxWarning>
</WarningBox>
);
}
return (
<Fragment>
<header>
{balances.length > 0 ? (
<Fragment>
{balances.length === 1 && (
<div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>
)}
{balances.length > 1 && (
<div class="title">
Balance:{" "}
<ul style={{ margin: 0 }}>
{balances.map((b, i) => (
<li key={i}>{b.available}</li>
))}
</ul>
</div>
)}
</Fragment>
) : (
<div />
)}
<div>
<ButtonPrimary onClick={onAddNewAction}>
<b>+</b>
</ButtonPrimary>
</div>
</header>
<section>
{Object.keys(byDate).map((d, i) => {
return (
<Fragment key={i}>
<DateSeparator>
<Time
timestamp={{ t_ms: Number.parseInt(d, 10) }}
format="dd MMMM yyyy"
/>
</DateSeparator>
{byDate[d].map((tx, i) => (
<TransactionItem
key={i}
tx={tx}
multiCurrency={multiCurrency}
/>
))}
</Fragment>
);
})}
<p
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
{currencies.length === 1 ? (
<div style={{ fontSize: "large" }}>{selectedCurrency}</div>
) : (
<NiceSelect>
<select
value={currencyIndex}
onChange={(e) => {
setCurrencyIndex(Number(e.currentTarget.value));
}}
>
{currencies.map((currency, index) => {
return (
<option value={index} key={currency}>
{currency}
</option>
);
})}
</select>
</NiceSelect>
)}
{currencyAmount && (
<h2 style={{ margin: 0 }}>
{Amounts.stringifyValue(currencyAmount)}
</h2>
)}
</p>
<div style={{ marginLeft: "auto", width: "fit-content" }}>
<ButtonPrimary
onClick={() => goToWalletManualWithdraw(selectedCurrency)}
>
Withdraw
</ButtonPrimary>
{currencyAmount && Amounts.isNonZero(currencyAmount) && (
<ButtonBoxPrimary
onClick={() => goToWalletDeposit(selectedCurrency)}
>
Deposit
</ButtonBoxPrimary>
)}
</div>
</section>
{datesWithTransaction.length === 0 ? (
<section>There is no history for this currency</section>
) : (
<section>
{datesWithTransaction.map((d, i) => {
return (
<Fragment key={i}>
<DateSeparator>
<Time
timestamp={{ t_ms: Number.parseInt(d, 10) }}
format="dd MMMM yyyy"
/>
</DateSeparator>
{byDate[d].map((tx, i) => (
<TransactionItem
key={i}
tx={tx}
multiCurrency={multiCurrency}
/>
))}
</Fragment>
);
})}
</section>
)}
</Fragment>
);
}

View File

@ -0,0 +1,33 @@
/*
This file is part of GNU Taler
(C) 2021 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 { createExample } from "../test-utils";
import { queryToSlashKeys } from "../utils/index";
import { LastActivityPage as TestedComponent } from "./LastActivityPage";
export default {
title: "wallet/last activity",
component: TestedComponent,
};
export const InitialState = createExample(TestedComponent, {
onVerify: queryToSlashKeys,
});

View File

@ -0,0 +1,35 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ButtonPrimary } from "../components/styled";
import { AddNewActionView } from "./AddNewActionView";
export function LastActivityPage(): VNode {
const [addingAction, setAddingAction] = useState(false);
if (addingAction) {
return <AddNewActionView onCancel={() => setAddingAction(false)} />;
}
return (
<section>
<div />
<ButtonPrimary onClick={() => setAddingAction(true)}>+</ButtonPrimary>
</section>
);
}

View File

@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { VNode, h } from "preact";
import { VNode, h, Fragment } from "preact";
import { useState } from "preact/hooks";
import { CreateManualWithdraw } from "./CreateManualWithdraw";
import * as wxApi from "../wxApi";
@ -29,8 +29,10 @@ import { route } from "preact-router";
import { Pages } from "../NavigationBar";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import { ExchangeAddPage } from "./ExchangeAddPage";
import { Loading } from "../components/Loading";
import { ErrorBox } from "../components/styled";
export function ManualWithdrawPage(): VNode {
export function ManualWithdrawPage({ currency }: { currency?: string }): VNode {
const [success, setSuccess] = useState<
| {
response: AcceptManualWithdrawalResult;
@ -86,10 +88,15 @@ export function ManualWithdrawPage(): VNode {
}
if (!state) {
return <div>loading...</div>;
return <Loading />;
}
if (state.hasError) {
return <div>There was an error getting the known exchanges</div>;
return (
<Fragment>
<ErrorBox>{state.message}</ErrorBox>
<p>There was an error getting the known exchanges</p>
</Fragment>
);
}
const exchangeList = state.response.exchanges.reduce(
(p, c) => ({
@ -105,6 +112,7 @@ export function ManualWithdrawPage(): VNode {
error={error}
exchangeList={exchangeList}
onCreate={doCreate}
initialCurrency={currency}
/>
);
}

View File

@ -73,7 +73,7 @@ export function TransactionPage({ tid }: { tid: string }): VNode {
}
if (state.hasError) {
route(Pages.history);
route(Pages.balance);
return (
<div>
<i18n.Translate>
@ -84,7 +84,16 @@ export function TransactionPage({ tid }: { tid: string }): VNode {
}
function goToHistory(): void {
route(Pages.history);
const currency =
state !== undefined && !state.hasError
? Amounts.parseOrThrow(state.response.amountRaw).currency
: undefined;
if (currency) {
route(Pages.balance_history.replace(":currency", currency));
} else {
route(Pages.balance);
}
}
return (

View File

@ -33,5 +33,22 @@ import * as a11 from "./ReserveCreated.stories";
import * as a12 from "./Settings.stories";
import * as a13 from "./Transaction.stories";
import * as a14 from "./Welcome.stories";
import * as a15 from "./AddNewActionView.stories";
export default [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14];
export default [
a1,
a2,
a3,
a4,
a5,
a6,
a7,
a8,
a9,
a10,
a11,
a12,
a13,
a14,
a15,
];

View File

@ -22,31 +22,32 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
import { Fragment, h, render, VNode } from "preact";
import { h, render, VNode } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { LogoHeader } from "./components/LogoHeader";
import { SuccessBox, WalletBox } from "./components/styled";
import { DevContextProvider } from "./context/devContext";
import { IoCProviderForRuntime } from "./context/iocContext";
import { PayPage } from "./cta/Pay";
import { RefundPage } from "./cta/Refund";
import { TipPage } from "./cta/Tip";
import { WithdrawPage } from "./cta/Withdraw";
import { strings } from "./i18n/strings";
import { Pages, WalletNavBar } from "./NavigationBar";
import { DeveloperPage } from "./popup/DeveloperPage";
import { BackupPage } from "./wallet/BackupPage";
import { BalancePage } from "./wallet/BalancePage";
import { DepositPage } from "./wallet/DepositPage";
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
import { HistoryPage } from "./wallet/History";
import { LastActivityPage } from "./wallet/LastActivityPage";
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage";
import { ProviderAddPage } from "./wallet/ProviderAddPage";
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
import { SettingsPage } from "./wallet/Settings";
import { TransactionPage } from "./wallet/Transaction";
import { WelcomePage } from "./wallet/Welcome";
import { BackupPage } from "./wallet/BackupPage";
import { DeveloperPage } from "./popup/DeveloperPage";
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage";
import { WalletBox } from "./components/styled";
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
import { ProviderAddPage } from "./wallet/ProviderAddPage";
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
import { DepositPage } from "./wallet/DepositPage";
import { IoCProviderForRuntime } from "./context/iocContext";
function main(): void {
try {
@ -71,140 +72,156 @@ if (document.readyState === "loading") {
main();
}
function withLogoAndNavBar(Component: any) {
return function withLogoAndNavBarComponent(props: any): VNode {
return (
<Fragment>
<LogoHeader />
<WalletNavBar />
<WalletBox>
<Component {...props} />
</WalletBox>
</Fragment>
);
};
}
function Application(): VNode {
const [globalNotification, setGlobalNotification] = useState<
string | undefined
>(undefined);
return (
<div>
<DevContextProvider>
<IoCProviderForRuntime>
<Router history={createHashHistory()}>
<Route
path={Pages.welcome}
component={withLogoAndNavBar(WelcomePage)}
/>
{({ devMode }: { devMode: boolean }) => (
<IoCProviderForRuntime>
<LogoHeader />
<WalletNavBar devMode={devMode} />
<WalletBox>
{globalNotification && (
<SuccessBox onClick={() => setGlobalNotification(undefined)}>
<div>{globalNotification}</div>
</SuccessBox>
)}
<Router history={createHashHistory()}>
<Route path={Pages.welcome} component={WelcomePage} />
<Route
path={Pages.history}
component={withLogoAndNavBar(HistoryPage)}
/>
<Route
path={Pages.transaction}
component={withLogoAndNavBar(TransactionPage)}
/>
<Route
path={Pages.balance}
component={withLogoAndNavBar(BalancePage)}
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
goToWalletDeposit={(currency: string) =>
route(Pages.deposit.replace(":currency", currency))
}
/>
<Route
path={Pages.settings}
component={withLogoAndNavBar(SettingsPage)}
/>
<Route
path={Pages.backup}
component={withLogoAndNavBar(BackupPage)}
onAddProvider={() => {
route(Pages.provider_add);
}}
/>
<Route
path={Pages.provider_detail}
component={withLogoAndNavBar(ProviderDetailPage)}
onBack={() => {
route(Pages.backup);
}}
/>
<Route
path={Pages.provider_add}
component={withLogoAndNavBar(ProviderAddPage)}
onBack={() => {
route(Pages.backup);
}}
/>
<Route
path={Pages.balance}
component={BalancePage}
goToWalletManualWithdraw={() =>
route(Pages.manual_withdraw.replace(":currency?", ""))
}
goToWalletDeposit={(currency: string) =>
route(Pages.deposit.replace(":currency", currency))
}
goToWalletHistory={(currency: string) =>
route(Pages.balance_history.replace(":currency", currency))
}
/>
<Route
path={Pages.balance_history}
component={HistoryPage}
goToWalletDeposit={(currency: string) =>
route(Pages.deposit.replace(":currency", currency))
}
goToWalletManualWithdraw={(currency?: string) =>
route(
Pages.manual_withdraw.replace(
":currency?",
currency || "",
),
)
}
/>
<Route
path={Pages.last_activity}
component={LastActivityPage}
/>
<Route path={Pages.transaction} component={TransactionPage} />
<Route path={Pages.settings} component={SettingsPage} />
<Route
path={Pages.backup}
component={BackupPage}
onAddProvider={() => {
route(Pages.provider_add);
}}
/>
<Route
path={Pages.provider_detail}
component={ProviderDetailPage}
onBack={() => {
route(Pages.backup);
}}
/>
<Route
path={Pages.provider_add}
component={ProviderAddPage}
onBack={() => {
route(Pages.backup);
}}
/>
<Route
path={Pages.exchange_add}
component={withLogoAndNavBar(ExchangeAddPage)}
onBack={() => {
route(Pages.balance);
}}
/>
<Route
path={Pages.exchange_add}
component={ExchangeAddPage}
onBack={() => {
route(Pages.balance);
}}
/>
<Route
path={Pages.manual_withdraw}
component={withLogoAndNavBar(ManualWithdrawPage)}
/>
<Route
path={Pages.manual_withdraw}
component={ManualWithdrawPage}
/>
<Route
path={Pages.deposit}
component={withLogoAndNavBar(DepositPage)}
/>
<Route
path={Pages.reset_required}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.payback}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.return_coins}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.deposit}
component={DepositPage}
onSuccess={(currency: string) => {
route(Pages.balance_history.replace(":currency", currency));
setGlobalNotification(
"All done, your transaction is in progress",
);
}}
/>
<Route
path={Pages.reset_required}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.payback}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.return_coins}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.dev}
component={withLogoAndNavBar(DeveloperPage)}
/>
<Route path={Pages.dev} component={DeveloperPage} />
{/** call to action */}
<Route
path={Pages.pay}
component={PayPage}
goToWalletManualWithdraw={() =>
goToWalletPage(Pages.manual_withdraw)
}
/>
<Route path={Pages.refund} component={RefundPage} />
<Route path={Pages.tips} component={TipPage} />
<Route path={Pages.withdraw} component={WithdrawPage} />
{/** call to action */}
<Route
path={Pages.pay}
component={PayPage}
goToWalletManualWithdraw={(currency?: string) =>
route(
Pages.manual_withdraw.replace(
":currency?",
currency || "",
),
)
}
goBack={() => route(Pages.balance)}
/>
<Route
path={Pages.pay}
component={PayPage}
goBack={() => route(Pages.balance)}
/>
<Route path={Pages.refund} component={RefundPage} />
<Route path={Pages.tips} component={TipPage} />
<Route path={Pages.withdraw} component={WithdrawPage} />
<Route default component={Redirect} to={Pages.history} />
</Router>
</IoCProviderForRuntime>
<Route default component={Redirect} to={Pages.balance} />
</Router>
</WalletBox>
</IoCProviderForRuntime>
)}
</DevContextProvider>
</div>
);
}
function goToWalletPage(page: Pages | string): null {
// eslint-disable-next-line no-undef
chrome.tabs.create({
active: true,
// eslint-disable-next-line no-undef
url: chrome.extension.getURL(`/static/wallet.html#${page}`),
});
return null;
}
function Redirect({ to }: { to: string }): null {
useEffect(() => {
console.log("go some wrong route");
route(to, true);
});
return null;

View File

@ -24,7 +24,7 @@
import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
AddExchangeRequest, AmountJson, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
AddExchangeRequest, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, ExchangesListRespose,
GetExchangeTosResult, GetExchangeWithdrawalInfo,
GetFeeForDepositRequest,