reserveCreated new design

This commit is contained in:
Sebastian 2021-11-16 13:59:53 -03:00
parent c33ed91971
commit a994009d2f
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
37 changed files with 737 additions and 559 deletions

View File

@ -16,12 +16,31 @@
import { URLSearchParams } from "./url.js";
interface PaytoUri {
export type PaytoUri = PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank;
interface PaytoUriGeneric {
targetType: string;
targetPath: string;
params: { [name: string]: string };
}
interface PaytoUriUnknown extends PaytoUriGeneric {
isKnown: false;
}
interface PaytoUriIBAN extends PaytoUriGeneric {
isKnown: true;
targetType: 'iban',
iban: string;
}
interface PaytoUriTalerBank extends PaytoUriGeneric {
isKnown: true;
targetType: 'x-taler-bank',
host: string;
account: string;
}
const paytoPfx = "payto://";
/**
@ -63,9 +82,33 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
params[v] = k;
});
if (targetType === 'x-taler-bank') {
const parts = targetPath.split('/')
const host = parts[0]
const account = parts[1]
return {
targetPath,
targetType,
params,
isKnown: true,
host, account,
};
}
if (targetType === 'iban') {
return {
isKnown: true,
targetPath,
targetType,
params,
iban: targetPath
};
}
return {
targetPath,
targetType,
params,
isKnown: false
};
}

View File

@ -25,10 +25,10 @@
* Imports.
*/
import { i18n } from "@gnu-taler/taler-util";
import { ComponentChildren, JSX, h } from "preact";
import { ComponentChildren, h, VNode } from "preact";
import Match from "preact-router/match";
import { useDevContext } from "./context/devContext";
import { PopupNavigation } from "./components/styled";
import { useDevContext } from "./context/devContext";
export enum Pages {
welcome = "/welcome",
@ -59,7 +59,7 @@ interface TabProps {
children?: ComponentChildren;
}
function Tab(props: TabProps): JSX.Element {
function Tab(props: TabProps): VNode {
let cssClass = "";
if (props.current?.startsWith(props.target)) {
cssClass = "active";

View File

@ -14,8 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX } from "preact/jsx-runtime";
import { h } from "preact";
import { h, VNode } from "preact";
interface Props {
enabled: boolean;
@ -30,7 +29,7 @@ export function Checkbox({
onToggle,
label,
description,
}: Props): JSX.Element {
}: Props): VNode {
return (
<div>
<input

View File

@ -14,9 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX } from "preact/jsx-runtime";
import { Outlined, StyledCheckboxLabel } from "./styled/index";
import { h } from "preact";
import { h, VNode } from "preact";
interface Props {
enabled: boolean;
@ -47,7 +46,7 @@ export function CheckboxOutlined({
enabled,
onToggle,
label,
}: Props): JSX.Element {
}: Props): VNode {
return (
<Outlined>
<StyledCheckboxLabel onClick={onToggle}>

View File

@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX, h } from "preact";
import { h, VNode } from "preact";
export function DebugCheckbox({
enabled,
@ -22,7 +22,7 @@ export function DebugCheckbox({
}: {
enabled: boolean;
onToggle: () => void;
}): JSX.Element {
}): VNode {
return (
<div>
<input

View File

@ -15,8 +15,7 @@
*/
import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { h } from "preact";
import { JSX } from "preact/jsx-runtime";
import { Fragment, h, VNode } from "preact";
import { PageLink } from "../renderHtml";
interface Props {
@ -24,51 +23,47 @@ interface Props {
diagnostics: WalletDiagnostics | undefined;
}
export function Diagnostics({
timedOut,
diagnostics,
}: Props): JSX.Element | null {
export function Diagnostics({ timedOut, diagnostics }: Props): VNode {
if (timedOut) {
return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
}
if (diagnostics) {
if (diagnostics.errors.length === 0) {
return null;
} else {
return (
<div
style={{
borderLeft: "0.5em solid red",
paddingLeft: "1em",
paddingTop: "0.2em",
paddingBottom: "0.2em",
}}
>
<p>Problems detected:</p>
<ol>
{diagnostics.errors.map((errMsg) => (
<li key={errMsg}>{errMsg}</li>
))}
</ol>
{diagnostics.firefoxIdbProblem ? (
<p>
Please check in your <code>about:config</code> settings that you
have IndexedDB enabled (check the preference name{" "}
<code>dom.indexedDB.enabled</code>).
</p>
) : null}
{diagnostics.dbOutdated ? (
<p>
Your wallet database is outdated. Currently automatic migration is
not supported. Please go{" "}
<PageLink pageName="/reset-required">here</PageLink> to reset the
wallet database.
</p>
) : null}
</div>
);
return <Fragment />;
}
return (
<div
style={{
borderLeft: "0.5em solid red",
paddingLeft: "1em",
paddingTop: "0.2em",
paddingBottom: "0.2em",
}}
>
<p>Problems detected:</p>
<ol>
{diagnostics.errors.map((errMsg) => (
<li key={errMsg}>{errMsg}</li>
))}
</ol>
{diagnostics.firefoxIdbProblem ? (
<p>
Please check in your <code>about:config</code> settings that you
have IndexedDB enabled (check the preference name{" "}
<code>dom.indexedDB.enabled</code>).
</p>
) : null}
{diagnostics.dbOutdated ? (
<p>
Your wallet database is outdated. Currently automatic migration is
not supported. Please go{" "}
<PageLink pageName="/reset-required">here</PageLink> to reset the
wallet database.
</p>
) : null}
</div>
);
}
return <p>Running diagnostics ...</p>;

View File

@ -14,9 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h } from "preact";
import { h, VNode } from "preact";
import { useRef, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
interface Props {
value: string;
@ -31,31 +30,35 @@ export function EditableText({
onChange,
label,
description,
}: Props): JSX.Element {
}: Props): VNode {
const [editing, setEditing] = useState(false);
const ref = useRef<HTMLInputElement>(null);
let InputText;
if (!editing) {
InputText = () => (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>{value}</p>
<button onClick={() => setEditing(true)}>edit</button>
</div>
);
InputText = function InputToEdit(): VNode {
return (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>{value}</p>
<button onClick={() => setEditing(true)}>edit</button>
</div>
);
};
} else {
InputText = () => (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<input value={value} ref={ref} type="text" id={`text-${name}`} />
<button
onClick={() => {
if (ref.current)
onChange(ref.current.value).then((r) => setEditing(false));
}}
>
confirm
</button>
</div>
);
InputText = function InputEditing(): VNode {
return (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<input value={value} ref={ref} type="text" id={`text-${name}`} />
<button
onClick={() => {
if (ref.current)
onChange(ref.current.value).then(() => setEditing(false));
}}
>
confirm
</button>
</div>
);
};
}
return (
<div>

View File

@ -13,12 +13,10 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Fragment, VNode } from "preact";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { JSXInternal } from "preact/src/jsx";
import { h } from "preact";
export function ExchangeXmlTos({ doc }: { doc: Document }) {
export function ExchangeXmlTos({ doc }: { doc: Document }): VNode {
const termsNode = doc.querySelector("[ids=terms-of-service]");
if (!termsNode) {
return (
@ -70,7 +68,7 @@ function renderChild(child: Element): VNode {
default:
return (
<div style={{ color: "red", display: "hidden" }}>
unknown tag {child.nodeName} <a></a>
unknown tag {child.nodeName}
</div>
);
}
@ -81,10 +79,10 @@ function renderChild(child: Element): VNode {
* @returns
*/
function AnchorWithOpenState(
props: JSXInternal.HTMLAttributes<HTMLAnchorElement>,
) {
props: h.JSX.HTMLAttributes<HTMLAnchorElement>,
): VNode {
const [open, setOpen] = useState<boolean>(false);
function doClick(e: JSXInternal.TargetedMouseEvent<HTMLAnchorElement>) {
function doClick(e: h.JSX.TargetedMouseEvent<HTMLAnchorElement>): void {
setOpen(!open);
e.preventDefault();
}

View File

@ -14,9 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX } from "preact/jsx-runtime";
import { Fragment, h, VNode } from "preact";
import { NiceSelect } from "./styled/index";
import { h } from "preact";
interface Props {
value?: string;
@ -34,13 +33,12 @@ export function SelectList({
name,
value,
list,
canBeNull,
onChange,
label,
description,
}: Props): JSX.Element {
}: Props): VNode {
return (
<div>
<Fragment>
<label
htmlFor={`text-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
@ -84,6 +82,6 @@ export function SelectList({
{description}
</span>
)}
</div>
</Fragment>
);
}

View File

@ -0,0 +1,41 @@
/*
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 { Timestamp } from "@gnu-taler/taler-util";
import { formatISO, format } from "date-fns";
import { h, VNode } from "preact";
export function Time({
timestamp,
format: formatString,
}: {
timestamp: Timestamp | undefined;
format: string;
}): VNode {
return (
<time
dateTime={
!timestamp || timestamp.t_ms === "never"
? undefined
: formatISO(timestamp.t_ms)
}
>
{!timestamp || timestamp.t_ms === "never"
? "never"
: format(timestamp.t_ms, formatString)}
</time>
);
}

View File

@ -20,8 +20,7 @@ import {
Transaction,
TransactionType,
} from "@gnu-taler/taler-util";
import { format, formatDistance } from "date-fns";
import { h } from "preact";
import { h, VNode } from "preact";
import imageBank from "../../static/img/ri-bank-line.svg";
import imageHandHeart from "../../static/img/ri-hand-heart-line.svg";
import imageRefresh from "../../static/img/ri-refresh-line.svg";
@ -36,11 +35,12 @@ import {
LargeText,
LightText,
} from "./styled/index";
import { Time } from "./Time";
export function TransactionItem(props: {
tx: Transaction;
multiCurrency: boolean;
}): JSX.Element {
}): VNode {
const tx = props.tx;
switch (tx.type) {
case TransactionType.Withdrawal:
@ -125,10 +125,7 @@ export function TransactionItem(props: {
}
}
function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
const date = new Date(props.timestamp.t_ms);
const dateStr = format(date, "dd MMM, hh:mm");
function TransactionLayout(props: TransactionLayoutProps): VNode {
return (
<HistoryRow href={Pages.transaction.replace(":tid", props.id)}>
<img src={props.iconPath} />
@ -146,7 +143,9 @@ function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
Waiting for confirmation
</LightText>
)}
<SmallLightText style={{ marginTop: 5 }}>{dateStr}</SmallLightText>
<SmallLightText style={{ marginTop: 5 }}>
<Time timestamp={props.timestamp} format="dd MMM, hh:mm" />
</SmallLightText>
</Column>
<TransactionAmount
pending={props.pending}
@ -177,7 +176,7 @@ interface TransactionAmountProps {
multiCurrency: boolean;
}
function TransactionAmount(props: TransactionAmountProps): JSX.Element {
function TransactionAmount(props: TransactionAmountProps): VNode {
const [currency, amount] = props.amount.split(":");
let sign: string;
switch (props.debitCreditIndicator) {

View File

@ -85,7 +85,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
overflow: auto;
table td {
padding: 5px 10px;
padding: 5px 5px;
}
table tr {
border-bottom: 1px solid black;
@ -328,7 +328,8 @@ const ButtonVariant = styled(Button)`
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
`;
export const ButtonPrimary = styled(ButtonVariant)`
export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
font-size: ${({ small }) => (small ? "small" : "inherit")};
background-color: rgb(66, 184, 221);
`;
export const ButtonBoxPrimary = styled(ButtonBox)`
@ -501,29 +502,40 @@ export const Input = styled.div<{ invalid?: boolean }>`
`;
export const InputWithLabel = styled.div<{ invalid?: boolean }>`
/* display: flex; */
& label {
display: block;
font-weight: bold;
margin-left: 0.5em;
padding: 5px;
color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
& > div {
position: relative;
& div {
line-height: 24px;
display: flex;
top: 0px;
bottom: 0px;
& > div {
position: absolute;
background-color: lightgray;
padding: 5px;
margin: 2px;
}
& > input {
flex: 1;
padding: 5px;
border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
}
& div > span {
background-color: lightgray;
box-sizing: border-box;
border-bottom-left-radius: 0.25em;
border-top-left-radius: 0.25em;
height: 2em;
display: inline-block;
padding-left: 0.5em;
padding-right: 0.5em;
align-items: center;
display: flex;
}
& input {
border-width: 1px;
box-sizing: border-box;
height: 2em;
/* border-color: lightgray; */
border-bottom-right-radius: 0.25em;
border-top-right-radius: 0.25em;
border-color: ${({ invalid }) => (!invalid ? "lightgray" : "red")};
}
`;
@ -535,6 +547,7 @@ export const ErrorBox = styled.div`
flex-direction: column;
/* margin: 0.5em; */
padding: 1em;
margin: 1em;
/* width: 100%; */
color: #721c24;
background: #f8d7da;
@ -592,6 +605,8 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
}
`;
const image = `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`;
export const NiceSelect = styled.div`
& > select {
-webkit-appearance: none;
@ -600,11 +615,18 @@ export const NiceSelect = styled.div`
appearance: none;
outline: 0;
box-shadow: none;
background-image: none;
background-image: ${image};
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
background-color: white;
flex: 1;
padding: 0.5em 1em;
border-radius: 0.25rem;
font-size: 1em;
padding: 0.5em 3em 0.5em 1em;
cursor: pointer;
}
@ -613,27 +635,6 @@ export const NiceSelect = styled.div`
/* width: 10em; */
overflow: hidden;
border-radius: 0.25em;
&::after {
content: "\u25BC";
position: absolute;
top: 0;
right: 0;
padding: 0.5em 1em;
cursor: pointer;
pointer-events: none;
-webkit-transition: 0.25s all ease;
-o-transition: 0.25s all ease;
transition: 0.25s all ease;
}
&:hover::after {
/* color: #f39c12; */
}
&::-ms-expand {
display: none;
}
`;
export const Outlined = styled.div`

View File

@ -36,7 +36,7 @@ import {
PreparePayResult,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { h, Fragment, JSX, VNode } from "preact";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { LogoHeader } from "../components/LogoHeader";
import { Part } from "../components/Part";
@ -100,7 +100,7 @@ const doPayment = async (
return res;
};
export function PayPage({ talerPayUri }: Props): JSX.Element {
export function PayPage({ talerPayUri }: Props): VNode {
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(
undefined,
);
@ -159,7 +159,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
return <span>Loading payment information ...</span>;
}
const onClick = async () => {
const onClick = async (): Promise<void> => {
try {
const res = await doPayment(payStatus);
setPayResult(res);
@ -198,7 +198,7 @@ export function PaymentRequestView({
onClick,
payErrMsg,
balance,
}: PaymentRequestViewProps) {
}: PaymentRequestViewProps): VNode {
let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
const contractTerms: ContractTerms = payStatus.contractTerms;
@ -225,7 +225,7 @@ export function PaymentRequestView({
merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
}
function Alternative() {
function Alternative(): VNode {
const [showQR, setShowQR] = useState<boolean>(false);
const privateUri =
payStatus.status !== PreparePayResultType.AlreadyConfirmed
@ -246,7 +246,7 @@ export function PaymentRequestView({
);
}
function ButtonsSection() {
function ButtonsSection(): VNode {
if (payResult) {
if (payResult.type === ConfirmPayResultType.Pending) {
return (
@ -257,7 +257,7 @@ export function PaymentRequestView({
</section>
);
}
return null;
return <Fragment />;
}
if (payErrMsg) {
return (
@ -392,7 +392,7 @@ export function PaymentRequestView({
);
}
function amountToString(text: AmountLike) {
function amountToString(text: AmountLike): string {
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj, 2);
return `${amount} ${aj.currency}`;

View File

@ -20,12 +20,11 @@
* @author Florian Dold
*/
import * as wxApi from "../wxApi";
import { AmountView } from "../renderHtml";
import { ApplyRefundResponse, Amounts } from "@gnu-taler/taler-util";
import { Amounts, ApplyRefundResponse } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
import { h } from "preact";
import { AmountView } from "../renderHtml";
import * as wxApi from "../wxApi";
interface Props {
talerRefundUri?: string;
@ -33,7 +32,7 @@ interface Props {
export interface ViewProps {
applyResult: ApplyRefundResponse;
}
export function View({ applyResult }: ViewProps) {
export function View({ applyResult }: ViewProps): VNode {
return (
<section class="main">
<h1>GNU Taler Wallet</h1>
@ -58,7 +57,7 @@ export function View({ applyResult }: ViewProps) {
</section>
);
}
export function RefundPage({ talerRefundUri }: Props): JSX.Element {
export function RefundPage({ talerRefundUri }: Props): VNode {
const [applyResult, setApplyResult] = useState<
ApplyRefundResponse | undefined
>(undefined);
@ -71,9 +70,10 @@ export function RefundPage({ talerRefundUri }: Props): JSX.Element {
const result = await wxApi.applyRefund(talerRefundUri);
setApplyResult(result);
} catch (e) {
console.error(e);
setErrMsg(e.message);
console.log("err message", e.message);
if (e instanceof Error) {
setErrMsg(e.message);
console.log("err message", e.message);
}
}
};
doFetch();

View File

@ -20,12 +20,11 @@
* @author Florian Dold <dold@taler.net>
*/
import { useEffect, useState } from "preact/hooks";
import { PrepareTipResult } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { AmountView } from "../renderHtml";
import * as wxApi from "../wxApi";
import { JSX } from "preact/jsx-runtime";
import { h } from "preact";
interface Props {
talerTipUri?: string;
@ -35,7 +34,11 @@ export interface ViewProps {
onAccept: () => void;
onIgnore: () => void;
}
export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) {
export function View({
prepareTipResult,
onAccept,
onIgnore,
}: ViewProps): VNode {
return (
<section class="main">
<h1>GNU Taler Wallet</h1>
@ -64,7 +67,7 @@ export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) {
);
}
export function TipPage({ talerTipUri }: Props): JSX.Element {
export function TipPage({ talerTipUri }: Props): VNode {
const [updateCounter, setUpdateCounter] = useState<number>(0);
const [prepareTipResult, setPrepareTipResult] = useState<
PrepareTipResult | undefined

View File

@ -19,10 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { amountFractionalBase, Amounts } from "@gnu-taler/taler-util";
import { ExchangeRecord } from "@gnu-taler/taler-wallet-core";
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
import { getMaxListeners } from "process";
import { amountFractionalBase } from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { View as TestedComponent } from "./Withdraw";
@ -793,12 +790,6 @@ export const NewTerms = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -810,7 +801,9 @@ export const NewTerms = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",
@ -834,12 +827,6 @@ export const TermsReviewingPLAIN = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -851,7 +838,9 @@ export const TermsReviewingPLAIN = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "plain",
@ -876,12 +865,6 @@ export const TermsReviewingHTML = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -893,7 +876,9 @@ export const TermsReviewingHTML = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "html",
@ -935,12 +920,6 @@ export const TermsReviewingPDF = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -952,7 +931,9 @@ export const TermsReviewingPDF = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "pdf",
@ -979,12 +960,6 @@ export const TermsReviewingXML = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -996,7 +971,9 @@ export const TermsReviewingXML = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",
@ -1021,12 +998,6 @@ export const NewTermsAccepted = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -1037,7 +1008,9 @@ export const NewTermsAccepted = createExample(TestedComponent, {
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",
@ -1062,12 +1035,6 @@ export const TermsShowAgainXML = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -1079,7 +1046,9 @@ export const TermsShowAgainXML = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",
@ -1105,12 +1074,6 @@ export const TermsChanged = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -1122,7 +1085,9 @@ export const TermsChanged = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",
@ -1146,12 +1111,6 @@ export const TermsNotFound = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -1163,7 +1122,9 @@ export const TermsNotFound = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
status: "notfound",
},
@ -1183,12 +1144,6 @@ export const TermsAlreadyAccepted = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: amountFractionalBase * 0.5,
@ -1200,7 +1155,9 @@ export const TermsAlreadyAccepted = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
status: "accepted",
},
@ -1220,12 +1177,6 @@ export const WithoutFee = createExample(TestedComponent, {
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
@ -1237,7 +1188,9 @@ export const WithoutFee = createExample(TestedComponent, {
fraction: 10000000,
},
onSwitchExchange: async () => {},
onSwitchExchange: async () => {
null;
},
terms: {
value: {
type: "xml",

View File

@ -29,9 +29,8 @@ import {
i18n,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { VNode, h, Fragment } from "preact";
import { useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { CheckboxOutlined } from "../components/CheckboxOutlined";
import { ExchangeXmlTos } from "../components/ExchangeToS";
import { LogoHeader } from "../components/LogoHeader";
@ -60,7 +59,6 @@ interface Props {
}
export interface ViewProps {
details: GetExchangeTosResult;
withdrawalFee: AmountJson;
exchangeBaseUrl: string;
amount: AmountJson;
@ -112,14 +110,13 @@ interface TermsDocumentPdf {
location: URL;
}
function amountToString(text: AmountJson) {
function amountToString(text: AmountJson): string {
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj);
return `${amount} ${aj.currency}`;
}
export function View({
details,
withdrawalFee,
exchangeBaseUrl,
knownExchanges,
@ -132,7 +129,7 @@ export function View({
onAccept,
reviewed,
confirmed,
}: ViewProps) {
}: ViewProps): VNode {
const needsReview = terms.status === "changed" || terms.status === "new";
const [switchingExchange, setSwitchingExchange] = useState<
@ -309,7 +306,7 @@ export function WithdrawPageWithParsedURI({
}: {
uri: string;
uriInfo: WithdrawUriInfoResponse;
}) {
}): VNode {
const [customExchange, setCustomExchange] = useState<string | undefined>(
undefined,
);
@ -407,7 +404,7 @@ export function WithdrawPageWithParsedURI({
return (
<View
onWithdraw={onWithdraw}
details={details.tos}
// details={details.tos}
amount={withdrawAmount}
exchangeBaseUrl={exchange}
withdrawalFee={details.info.withdrawFee} //FIXME

View File

@ -14,8 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX } from "preact/jsx-runtime";
import { h } from "preact";
import { h, VNode } from "preact";
/**
* View and edit auditors.
@ -27,6 +26,6 @@ import { h } from "preact";
* Imports.
*/
export function makePaybackPage(): JSX.Element {
export function makePaybackPage(): VNode {
return <div>not implemented</div>;
}

View File

@ -20,7 +20,7 @@
* @author Florian Dold
*/
import { Component, JSX, h } from "preact";
import { Component, h, VNode } from "preact";
import * as wxApi from "../wxApi";
interface State {
@ -45,7 +45,7 @@ class ResetNotification extends Component<any, State> {
const res = await wxApi.checkUpgrade();
this.setState({ resetRequired: res.dbResetRequired });
}
render(): JSX.Element {
render(): VNode {
if (this.state.resetRequired) {
return (
<div>
@ -92,6 +92,6 @@ class ResetNotification extends Component<any, State> {
/**
* @deprecated to be removed
*/
export function createResetRequiredPage(): JSX.Element {
export function createResetRequiredPage(): VNode {
return <ResetNotification />;
}

View File

@ -14,8 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX } from "preact/jsx-runtime";
import { h } from "preact";
import { h, VNode } from "preact";
/**
* Return coins to own bank account.
*
@ -25,6 +24,6 @@ import { h } from "preact";
/**
* Imports.
*/
export function createReturnCoinsPage(): JSX.Element {
export function createReturnCoinsPage(): VNode {
return <span>Not implemented yet.</span>;
}

View File

@ -24,18 +24,18 @@ import {
formatDuration,
intervalToDuration,
} from "date-fns";
import { Fragment, JSX, VNode, h } from "preact";
import { Fragment, h, VNode } from "preact";
import {
BoldLight,
ButtonPrimary,
ButtonSuccess,
Centered,
CenteredText,
CenteredBoldText,
CenteredText,
PopupBox,
RowBorderGray,
SmallText,
SmallLightText,
SmallText,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "../NavigationBar";
@ -72,8 +72,9 @@ export function BackupView({
return (
<PopupBox>
<section>
{providers.map((provider) => (
{providers.map((provider, idx) => (
<BackupLayout
key={idx}
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
@ -117,7 +118,7 @@ interface TransactionLayoutProps {
active: boolean;
}
function BackupLayout(props: TransactionLayoutProps): JSX.Element {
function BackupLayout(props: TransactionLayoutProps): VNode {
const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
const dateStr = date?.toLocaleString([], {
dateStyle: "medium",

View File

@ -18,17 +18,14 @@ import {
amountFractionalBase,
Amounts,
Balance,
BalancesResponse,
i18n,
} from "@gnu-taler/taler-util";
import { JSX, h, Fragment } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
import { h, VNode } from "preact";
import {
PopupBox,
Centered,
ButtonPrimary,
ErrorBox,
Middle,
PopupBox,
} from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
@ -37,7 +34,7 @@ export function BalancePage({
goToWalletManualWithdraw,
}: {
goToWalletManualWithdraw: () => void;
}) {
}): VNode {
const balance = useBalances();
return (
<BalanceView
@ -53,11 +50,11 @@ export interface BalanceViewProps {
goToWalletManualWithdraw: () => void;
}
function formatPending(entry: Balance): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
function formatPending(entry: Balance): VNode {
let incoming: VNode | undefined;
let payment: VNode | undefined;
const available = Amounts.parseOrThrow(entry.available);
// const available = Amounts.parseOrThrow(entry.available);
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
@ -105,8 +102,8 @@ export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
}: BalanceViewProps) {
function Content() {
}: BalanceViewProps): VNode {
function Content(): VNode {
if (!balance) {
return <span />;
}
@ -139,7 +136,7 @@ export function BalanceView({
return (
<section data-expanded data-centered>
<table style={{ width: "100%" }}>
{balance.response.balances.map((entry) => {
{balance.response.balances.map((entry, idx) => {
const av = Amounts.parseOrThrow(entry.available);
// Create our number formatter.
let formatter;
@ -168,7 +165,7 @@ export function BalanceView({
const fontSize =
v.length < 8 ? "3em" : v.length < 13 ? "2em" : "1em";
return (
<tr>
<tr key={idx}>
<td
style={{
height: 50,

View File

@ -14,12 +14,12 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX, h } from "preact";
import { h, VNode } from "preact";
import { Diagnostics } from "../components/Diagnostics";
import { useDiagnostics } from "../hooks/useDiagnostics.js";
import * as wxApi from "../wxApi";
export function DeveloperPage(props: any): JSX.Element {
export function DeveloperPage(): VNode {
const [status, timedOut] = useDiagnostics();
return (
<div>
@ -36,6 +36,7 @@ export function DeveloperPage(props: any): JSX.Element {
export function reload(): void {
try {
// eslint-disable-next-line no-undef
chrome.runtime.reload();
window.close();
} catch (e) {
@ -57,7 +58,9 @@ export async function confirmReset(): Promise<void> {
export function openExtensionPage(page: string) {
return () => {
// eslint-disable-next-line no-undef
chrome.tabs.create({
// eslint-disable-next-line no-undef
url: chrome.extension.getURL(page),
});
};

View File

@ -21,14 +21,14 @@ import {
Transaction,
TransactionsResponse,
} from "@gnu-taler/taler-util";
import { h, JSX } from "preact";
import { h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { PopupBox } from "../components/styled";
import { TransactionItem } from "../components/TransactionItem";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
export function HistoryPage(props: any): JSX.Element {
export function HistoryPage(): VNode {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);

View File

@ -28,13 +28,13 @@ import {
Amounts,
amountFractionalBase,
} from "@gnu-taler/taler-util";
import { Component, ComponentChildren, JSX, h } from "preact";
import { Component, ComponentChildren, h, VNode } from "preact";
/**
* Render amount as HTML, which non-breaking space between
* decimal value and currency.
*/
export function renderAmount(amount: AmountJson | string): JSX.Element {
export function renderAmount(amount: AmountJson | string): VNode {
let a;
if (typeof amount === "string") {
a = Amounts.parse(amount);
@ -56,13 +56,13 @@ export const AmountView = ({
amount,
}: {
amount: AmountJson | string;
}): JSX.Element => renderAmount(amount);
}): VNode => renderAmount(amount);
/**
* Abbreviate a string to a given length, and show the full
* string on hover as a tooltip.
*/
export function abbrev(s: string, n = 5): JSX.Element {
export function abbrev(s: string, n = 5): VNode {
let sAbbrev = s;
if (s.length > n) {
sAbbrev = s.slice(0, n) + "..";
@ -92,7 +92,7 @@ export class Collapsible extends Component<CollapsibleProps, CollapsibleState> {
super(props);
this.state = { collapsed: props.initiallyCollapsed };
}
render(): JSX.Element {
render(): VNode {
const doOpen = (e: any): void => {
this.setState({ collapsed: false });
e.preventDefault();
@ -132,19 +132,19 @@ interface ExpanderTextProps {
/**
* Show a heading with a toggle to show/hide the expandable content.
*/
export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
export function ExpanderText({ text }: ExpanderTextProps): VNode {
return <span>{text}</span>;
}
export interface LoadingButtonProps
extends JSX.HTMLAttributes<HTMLButtonElement> {
extends h.JSX.HTMLAttributes<HTMLButtonElement> {
isLoading: boolean;
}
export function ProgressButton({
isLoading,
...rest
}: LoadingButtonProps): JSX.Element {
}: LoadingButtonProps): VNode {
return (
<button class="pure-button pure-button-primary" type="button" {...rest}>
{isLoading ? (
@ -160,7 +160,8 @@ export function ProgressButton({
export function PageLink(props: {
pageName: string;
children?: ComponentChildren;
}): JSX.Element {
}): VNode {
// eslint-disable-next-line no-undef
const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);
return (
<a class="actionLink" href={url} target="_blank" rel="noopener noreferrer">

View File

@ -24,18 +24,17 @@ import {
formatDuration,
intervalToDuration,
} from "date-fns";
import { Fragment, JSX, VNode, h } from "preact";
import { Fragment, h, VNode } from "preact";
import {
BoldLight,
ButtonPrimary,
ButtonSuccess,
Centered,
CenteredText,
CenteredBoldText,
PopupBox,
CenteredText,
RowBorderGray,
SmallText,
SmallLightText,
SmallText,
WalletBox,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
@ -73,8 +72,9 @@ export function BackupView({
return (
<WalletBox>
<section>
{providers.map((provider) => (
{providers.map((provider, idx) => (
<BackupLayout
key={idx}
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
@ -118,7 +118,7 @@ interface TransactionLayoutProps {
active: boolean;
}
function BackupLayout(props: TransactionLayoutProps): JSX.Element {
function BackupLayout(props: TransactionLayoutProps): VNode {
const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
const dateStr = date?.toLocaleString([], {
dateStyle: "medium",

View File

@ -21,7 +21,7 @@ import {
BalancesResponse,
i18n,
} from "@gnu-taler/taler-util";
import { JSX, h } from "preact";
import { h, VNode } from "preact";
import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
@ -30,7 +30,7 @@ export function BalancePage({
goToWalletManualWithdraw,
}: {
goToWalletManualWithdraw: () => void;
}) {
}): VNode {
const balance = useBalances();
return (
<BalanceView
@ -51,7 +51,7 @@ export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
}: BalanceViewProps) {
}: BalanceViewProps): VNode {
if (!balance) {
return <span />;
}
@ -85,13 +85,13 @@ export function BalanceView({
);
}
function formatPending(entry: Balance): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
function formatPending(entry: Balance): VNode {
let incoming: VNode | undefined;
let payment: VNode | undefined;
const available = Amounts.parseOrThrow(entry.available);
// const available = Amounts.parseOrThrow(entry.available);
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
// const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
if (!Amounts.isZero(pendingIncoming)) {
incoming = (
@ -128,7 +128,7 @@ function ShowBalances({
}: {
wallet: BalancesResponse;
onWithdraw: () => void;
}) {
}): VNode {
return (
<WalletBox>
<section>

View File

@ -28,26 +28,27 @@ export default {
argTypes: {},
};
export const InitialState = createExample(TestedComponent, {});
// ,
const exchangeList = {
"http://exchange.taler:8081": "COL",
"http://exchange.tal": "EUR",
};
export const WithExchangeFilled = createExample(TestedComponent, {
currency: "COL",
initialExchange: "http://exchange.taler:8081",
export const InitialState = createExample(TestedComponent, {
exchangeList,
});
export const WithExchangeAndAmountFilled = createExample(TestedComponent, {
currency: "COL",
initialExchange: "http://exchange.taler:8081",
export const WithAmountInitialized = createExample(TestedComponent, {
initialAmount: "10",
exchangeList,
});
export const WithExchangeError = createExample(TestedComponent, {
initialExchange: "http://exchange.tal",
error: "The exchange url seems invalid",
exchangeList,
});
export const WithAmountError = createExample(TestedComponent, {
currency: "COL",
initialExchange: "http://exchange.taler:8081",
initialAmount: "e",
exchangeList,
});

View File

@ -20,9 +20,10 @@
*/
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage";
import { SelectList } from "../components/SelectList";
import {
ButtonPrimary,
Input,
@ -33,32 +34,56 @@ import {
export interface Props {
error: string | undefined;
currency: string | undefined;
initialExchange?: string;
initialAmount?: string;
onExchangeChange: (exchange: string) => void;
exchangeList: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
}
export function CreateManualWithdraw({
onExchangeChange,
initialExchange,
initialAmount,
exchangeList,
error,
currency,
onCreate,
}: Props): VNode {
const exchangeSelectList = Object.keys(exchangeList);
const currencySelectList = Object.values(exchangeList);
const exchangeMap = exchangeSelectList.reduce(
(p, c) => ({ ...p, [c]: `${c} (${exchangeList[c]})` }),
{} as Record<string, string>,
);
const currencyMap = currencySelectList.reduce(
(p, c) => ({ ...p, [c]: c }),
{} as Record<string, string>,
);
const initialExchange =
exchangeSelectList.length > 0 ? exchangeSelectList[0] : "";
const [exchange, setExchange] = useState(initialExchange || "");
const [currency, setCurrency] = useState(exchangeList[initialExchange] ?? "");
const [amount, setAmount] = useState(initialAmount || "");
const parsedAmount = Amounts.parse(`${currency}:${amount}`);
let timeout = useRef<number | undefined>(undefined);
useEffect(() => {
if (timeout) window.clearTimeout(timeout.current);
timeout.current = window.setTimeout(async () => {
onExchangeChange(exchange);
}, 1000);
}, [exchange]);
function changeExchange(exchange: string): void {
setExchange(exchange);
setCurrency(exchangeList[exchange]);
}
function changeCurrency(currency: string): void {
setCurrency(currency);
const found = Object.entries(exchangeList).find((e) => e[1] === currency);
if (found) {
setExchange(found[0]);
} else {
setExchange("");
}
}
if (!initialExchange) {
return <div>There is no known exchange where to withdraw, add one</div>;
}
return (
<WalletBox>
@ -73,26 +98,38 @@ export function CreateManualWithdraw({
withdraw the coins
</LightText>
<p>
<Input invalid={!!exchange && !currency}>
<label>Exchange</label>
<input
type="text"
placeholder="https://"
value={exchange}
onChange={(e) => setExchange(e.currentTarget.value)}
<Input>
<SelectList
label="Currency"
list={currencyMap}
name="currency"
value={currency}
onChange={changeCurrency}
/>
<small>http://exchange.taler:8081</small>
</Input>
<Input>
<SelectList
label="Exchange"
list={exchangeMap}
name="currency"
value={exchange}
onChange={changeExchange}
/>
</Input>
{/* <p style={{ display: "flex", justifyContent: "right" }}>
<a href="" style={{ marginLeft: "auto" }}>
Add new exchange
</a>
</p> */}
{currency && (
<InputWithLabel invalid={!!amount && !parsedAmount}>
<label>Amount</label>
<div>
<div>{currency}</div>
<span>{currency}</span>
<input
type="number"
style={{ paddingLeft: `${currency.length}em` }}
value={amount}
onChange={(e) => setAmount(e.currentTarget.value)}
onInput={(e) => setAmount(e.currentTarget.value)}
/>
</div>
</InputWithLabel>
@ -105,7 +142,7 @@ export function CreateManualWithdraw({
disabled={!parsedAmount || !exchange}
onClick={() => onCreate(exchange, parsedAmount!)}
>
Create
Start withdrawal
</ButtonPrimary>
</footer>
</WalletBox>

View File

@ -20,15 +20,15 @@ import {
Transaction,
TransactionsResponse,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { Fragment, h, JSX } from "preact";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { DateSeparator, WalletBox } from "../components/styled";
import { Time } from "../components/Time";
import { TransactionItem } from "../components/TransactionItem";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
export function HistoryPage(props: any): JSX.Element {
export function HistoryPage(): VNode {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
@ -57,24 +57,30 @@ export function HistoryPage(props: any): JSX.Element {
);
}
function amountToString(c: AmountString) {
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,
balances,
}: {
list: Transaction[];
balances: Balance[];
}) {
const byDate = list.reduce(function (rv, x) {
}): VNode {
const byDate = list.reduce((rv, x) => {
const theDate =
x.timestamp.t_ms === "never"
? "never"
: format(x.timestamp.t_ms, "dd MMMM yyyy");
(rv[theDate] = rv[theDate] || []).push(x);
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[] });
@ -93,8 +99,8 @@ export function HistoryView({
<div class="title">
Balance:{" "}
<ul style={{ margin: 0 }}>
{balances.map((b) => (
<li>{b.available}</li>
{balances.map((b, i) => (
<li key={i}>{b.available}</li>
))}
</ul>
</div>
@ -105,7 +111,12 @@ export function HistoryView({
{Object.keys(byDate).map((d, i) => {
return (
<Fragment key={i}>
<DateSeparator>{d}</DateSeparator>
<DateSeparator>
<Time
timestamp={{ t_ms: Number.parseInt(d, 10) }}
format="dd MMMM yyyy"
/>
</DateSeparator>
{byDate[d].map((tx, i) => (
<TransactionItem
key={i}

View File

@ -26,44 +26,31 @@ import {
import { ReserveCreated } from "./ReserveCreated.js";
import { route } from "preact-router";
import { Pages } from "../NavigationBar.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
interface Props {}
export function ManualWithdrawPage({}: Props): VNode {
export function ManualWithdrawPage(): VNode {
const [success, setSuccess] = useState<
AcceptManualWithdrawalResult | undefined
| {
response: AcceptManualWithdrawalResult;
exchangeBaseUrl: string;
amount: AmountJson;
}
| undefined
>(undefined);
const [currency, setCurrency] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
async function onExchangeChange(exchange: string | undefined): Promise<void> {
if (!exchange) return;
try {
const r = await fetch(`${exchange}/keys`);
const j = await r.json();
if (j.currency) {
await wxApi.addExchange({
exchangeBaseUrl: `${exchange}/`,
forceUpdate: true,
});
setCurrency(j.currency);
}
} catch (e) {
setError("The exchange url seems invalid");
setCurrency(undefined);
}
}
const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
async function doCreate(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<void> {
try {
const resp = await wxApi.acceptManualWithdrawal(
const response = await wxApi.acceptManualWithdrawal(
exchangeBaseUrl,
Amounts.stringify(amount),
);
setSuccess(resp);
setSuccess({ exchangeBaseUrl, response, amount });
} catch (e) {
if (e instanceof Error) {
setError(e.message);
@ -77,8 +64,10 @@ export function ManualWithdrawPage({}: Props): VNode {
if (success) {
return (
<ReserveCreated
reservePub={success.reservePub}
paytos={success.exchangePaytoUris}
reservePub={success.response.reservePub}
payto={success.response.exchangePaytoUris[0]}
exchangeBaseUrl={success.exchangeBaseUrl}
amount={success.amount}
onBack={() => {
route(Pages.balance);
}}
@ -86,12 +75,22 @@ export function ManualWithdrawPage({}: Props): VNode {
);
}
if (!knownExchangesHook || knownExchangesHook.hasError) {
return <div>No Known exchanges</div>;
}
const exchangeList = knownExchangesHook.response.exchanges.reduce(
(p, c) => ({
...p,
[c.exchangeBaseUrl]: c.currency,
}),
{} as Record<string, string>,
);
return (
<CreateManualWithdraw
error={error}
currency={currency}
exchangeList={exchangeList}
onCreate={doCreate}
onExchangeChange={onExchangeChange}
/>
);
}

View File

@ -14,23 +14,23 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { i18n } from "@gnu-taler/taler-util";
import {
ProviderInfo,
ProviderPaymentStatus,
ProviderPaymentType,
} from "@gnu-taler/taler-wallet-core";
import { format, formatDuration, intervalToDuration } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { Fragment, h, VNode } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
import {
Button,
ButtonDestructive,
ButtonPrimary,
PaymentStatus,
WalletBox,
SmallLightText,
WalletBox,
} from "../components/styled";
import { Time } from "../components/Time";
import { useProviderStatus } from "../hooks/useProviderStatus";
interface Props {
@ -97,10 +97,7 @@ export function ProviderView({
</header>
<section>
<p>
<b>Last backup:</b>{" "}
{lb == null || lb.t_ms == "never"
? "never"
: format(lb.t_ms, "dd MMM yyyy")}{" "}
<b>Last backup:</b> <Time timestamp={lb} format="dd MMMM yyyy" />
</p>
<ButtonPrimary onClick={onSync}>
<i18n.Translate>Back up</i18n.Translate>
@ -128,7 +125,7 @@ export function ProviderView({
<table>
<thead>
<tr>
<td></td>
<td>&nbsp;</td>
<td>
<i18n.Translate>old</i18n.Translate>
</td>
@ -174,32 +171,32 @@ export function ProviderView({
);
}
function daysSince(d?: Timestamp) {
if (!d || d.t_ms === "never") return "never synced";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
});
const str = formatDuration(duration, {
delimiter: ", ",
format: [
duration?.years
? i18n.str`years`
: duration?.months
? i18n.str`months`
: duration?.days
? i18n.str`days`
: duration?.hours
? i18n.str`hours`
: duration?.minutes
? i18n.str`minutes`
: i18n.str`seconds`,
],
});
return `synced ${str} ago`;
}
// function daysSince(d?: Timestamp): string {
// if (!d || d.t_ms === "never") return "never synced";
// const duration = intervalToDuration({
// start: d.t_ms,
// end: new Date(),
// });
// const str = formatDuration(duration, {
// delimiter: ", ",
// format: [
// duration?.years
// ? i18n.str`years`
// : duration?.months
// ? i18n.str`months`
// : duration?.days
// ? i18n.str`days`
// : duration?.hours
// ? i18n.str`hours`
// : duration?.minutes
// ? i18n.str`minutes`
// : i18n.str`seconds`,
// ],
// });
// return `synced ${str} ago`;
// }
function Error({ info }: { info: ProviderInfo }) {
function Error({ info }: { info: ProviderInfo }): VNode {
if (info.lastError) {
return <ErrorMessage title={info.lastError.hint} />;
}
@ -234,45 +231,45 @@ function Error({ info }: { info: ProviderInfo }) {
);
}
}
return null;
return <Fragment />;
}
function colorByStatus(status: ProviderPaymentType) {
switch (status) {
case ProviderPaymentType.InsufficientBalance:
return "rgb(223, 117, 20)";
case ProviderPaymentType.Unpaid:
return "rgb(202, 60, 60)";
case ProviderPaymentType.Paid:
return "rgb(28, 184, 65)";
case ProviderPaymentType.Pending:
return "gray";
case ProviderPaymentType.InsufficientBalance:
return "rgb(202, 60, 60)";
case ProviderPaymentType.TermsChanged:
return "rgb(202, 60, 60)";
}
}
// function colorByStatus(status: ProviderPaymentType): string {
// switch (status) {
// case ProviderPaymentType.InsufficientBalance:
// return "rgb(223, 117, 20)";
// case ProviderPaymentType.Unpaid:
// return "rgb(202, 60, 60)";
// case ProviderPaymentType.Paid:
// return "rgb(28, 184, 65)";
// case ProviderPaymentType.Pending:
// return "gray";
// // case ProviderPaymentType.InsufficientBalance:
// // return "rgb(202, 60, 60)";
// case ProviderPaymentType.TermsChanged:
// return "rgb(202, 60, 60)";
// }
// }
function descriptionByStatus(status: ProviderPaymentStatus) {
function descriptionByStatus(status: ProviderPaymentStatus): VNode {
switch (status.type) {
// return i18n.str`no enough balance to make the payment`
// return i18n.str`not paid yet`
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
if (status.paidUntil.t_ms === "never") {
return i18n.str`service paid`;
} else {
return (
<Fragment>
<b>Backup valid until:</b>{" "}
{format(status.paidUntil.t_ms, "dd MMM yyyy")}
</Fragment>
);
return <span>{i18n.str`service paid`}</span>;
}
return (
<Fragment>
<b>Backup valid until:</b>{" "}
<Time timestamp={status.paidUntil} format="dd MMM yyyy" />
</Fragment>
);
case ProviderPaymentType.Unpaid:
case ProviderPaymentType.InsufficientBalance:
case ProviderPaymentType.Pending:
return "";
return <span />;
}
}

View File

@ -28,10 +28,26 @@ export default {
argTypes: {},
};
export const InitialState = createExample(TestedComponent, {
reservePub: "ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ",
paytos: [
export const TalerBank = createExample(TestedComponent, {
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
payto:
"payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
"payto://x-taler-bank/international-bank.com/myaccount?amount=COL%3A1&message=Taler+Withdrawal+TYQTE7VA4M9GZQ4TR06YBNGA05AJGMFNSK4Q62NXR2FKNDB1J4EX",
],
amount: {
currency: "USD",
value: 10,
fraction: 0,
},
exchangeBaseUrl: "https://exchange.demo.taler.net",
});
export const IBAN = createExample(TestedComponent, {
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
payto:
"payto://iban/ASDQWEASDZXCASDQWE?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
amount: {
currency: "USD",
value: 10,
fraction: 0,
},
exchangeBaseUrl: "https://exchange.demo.taler.net",
});

View File

@ -1,66 +1,155 @@
import { h, Fragment, VNode } from "preact";
import { useState } from "preact/hooks";
import {
AmountJson,
Amounts,
parsePaytoUri,
PaytoUri,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { QR } from "../components/QR";
import { ButtonBox, FontIcon, WalletBox } from "../components/styled";
import {
ButtonDestructive,
ButtonPrimary,
WalletBox,
WarningBox,
} from "../components/styled";
export interface Props {
reservePub: string;
paytos: string[];
payto: string;
exchangeBaseUrl: string;
amount: AmountJson;
onBack: () => void;
}
export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode {
const [opened, setOpened] = useState(-1);
interface BankDetailsProps {
payto: PaytoUri;
exchangeBaseUrl: string;
subject: string;
amount: string;
}
function Row({
name,
value,
literal,
}: {
name: string;
value: string;
literal?: boolean;
}): VNode {
const [copied, setCopied] = useState(false);
function copyText(): void {
navigator.clipboard.writeText(value);
setCopied(true);
}
useEffect(() => {
setTimeout(() => {
setCopied(false);
}, 1000);
}, [copied]);
return (
<tr>
<td>
{!copied ? (
<ButtonPrimary small onClick={copyText}>
&nbsp; Copy &nbsp;
</ButtonPrimary>
) : (
<ButtonPrimary small disabled>
Copied
</ButtonPrimary>
)}
</td>
<td>
<b>{name}</b>
</td>
{literal ? (
<td>
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
{value}
</pre>
</td>
) : (
<td>{value}</td>
)}
</tr>
);
}
function BankDetailsByPaytoType({
payto,
subject,
exchangeBaseUrl,
amount,
}: BankDetailsProps): VNode {
const firstPart = !payto.isKnown ? (
<Fragment>
<Row name="Account" value={payto.targetPath} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : payto.targetType === "x-taler-bank" ? (
<Fragment>
<Row name="Bank host" value={payto.host} />
<Row name="Bank account" value={payto.account} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : payto.targetType === "iban" ? (
<Fragment>
<Row name="IBAN" value={payto.iban} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : undefined;
return (
<table>
{firstPart}
<Row name="Amount" value={amount} />
<Row name="Subject" value={subject} literal />
</table>
);
}
export function ReserveCreated({
reservePub,
payto,
onBack,
exchangeBaseUrl,
amount,
}: Props): VNode {
const paytoURI = parsePaytoUri(payto);
// const url = new URL(paytoURI?.targetPath);
if (!paytoURI) {
return <div>could not parse payto uri from exchange {payto}</div>;
}
return (
<WalletBox>
<section>
<h2>Reserve created!</h2>
<h1>Bank transfer details</h1>
<p>
Now you need to send money to the exchange to one of the following
accounts
</p>
<p>
To complete the setup of the reserve, you must now initiate a wire
transfer using the given wire transfer subject and crediting the
specified amount to the indicated account of the exchange.
Please wire <b>{Amounts.stringify(amount)}</b> to:
</p>
<BankDetailsByPaytoType
amount={Amounts.stringify(amount)}
exchangeBaseUrl={exchangeBaseUrl}
payto={paytoURI}
subject={reservePub}
/>
</section>
<section>
<ul>
{paytos.map((href, idx) => {
const url = new URL(href);
return (
<li key={idx}>
<p>
<a
href=""
onClick={(e) => {
setOpened((o) => (o === idx ? -1 : idx));
e.preventDefault();
}}
>
{url.pathname}
</a>
{opened === idx && (
<Fragment>
<p>
If your system supports RFC 8905, you can do this by
opening <a href={href}>this URI</a> or scan the QR with
your wallet
</p>
<QR text={href} />
</Fragment>
)}
</p>
</li>
);
})}
</ul>
<p>
<WarningBox>
Make sure to use the correct subject, otherwise the money will not
arrive in this wallet.
</WarningBox>
</p>
<p>
Alternative, you can also scan this QR code or open{" "}
<a href={payto}>this link</a> if you have a banking app installed that
supports RFC 8905
</p>
<QR text={payto} />
</section>
<footer>
<ButtonBox onClick={onBack}>
<FontIcon>&#x2190;</FontIcon>
</ButtonBox>
<div />
<ButtonDestructive onClick={onBack}>Cancel withdraw</ButtonDestructive>
</footer>
</WalletBox>
);

View File

@ -21,28 +21,27 @@ import {
Transaction,
TransactionType,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { JSX, VNode, h } from "preact";
import { h, VNode } from "preact";
import { route } from "preact-router";
import { useEffect, useState } from "preact/hooks";
import emptyImg from "../../static/img/empty.png";
import { ErrorMessage } from "../components/ErrorMessage";
import { Part } from "../components/Part";
import {
ButtonBox,
ButtonBoxDestructive,
Button,
ButtonDestructive,
ButtonPrimary,
FontIcon,
ListOfProducts,
RowBorderGray,
SmallLightText,
WalletBox,
WarningBox,
} from "../components/styled";
import { Time } from "../components/Time";
import { Pages } from "../NavigationBar";
import * as wxApi from "../wxApi";
export function TransactionPage({ tid }: { tid: string }): JSX.Element {
export function TransactionPage({ tid }: { tid: string }): VNode {
const [transaction, setTransaction] = useState<Transaction | undefined>(
undefined,
);
@ -70,8 +69,8 @@ export function TransactionPage({ tid }: { tid: string }): JSX.Element {
return (
<TransactionView
transaction={transaction}
onDelete={() => wxApi.deleteTransaction(tid).then((_) => history.go(-1))}
onRetry={() => wxApi.retryTransaction(tid).then((_) => history.go(-1))}
onDelete={() => wxApi.deleteTransaction(tid).then(() => history.go(-1))}
onRetry={() => wxApi.retryTransaction(tid).then(() => history.go(-1))}
onBack={() => {
route(Pages.history);
}}
@ -91,42 +90,42 @@ export function TransactionView({
onDelete,
onRetry,
onBack,
}: WalletTransactionProps) {
function TransactionTemplate({ children }: { children: VNode[] }) {
}: WalletTransactionProps): VNode {
function TransactionTemplate({ children }: { children: VNode[] }): VNode {
return (
<WalletBox>
<section style={{ padding: 8, textAlign: "center" }}>
<ErrorMessage title={transaction?.error?.hint} />
{transaction.pending && (
<WarningBox>This transaction is not completed</WarningBox>
<WarningBox>
This transaction is not completed
<a href="">more info...</a>
</WarningBox>
)}
</section>
<section>
<div style={{ textAlign: "center" }}>{children}</div>
</section>
<footer>
<ButtonBox onClick={onBack}>
<i18n.Translate>
{" "}
<FontIcon>&#x2190;</FontIcon>{" "}
</i18n.Translate>
</ButtonBox>
<Button onClick={onBack}>
<i18n.Translate> &lt; Back </i18n.Translate>
</Button>
<div>
{transaction?.error ? (
<ButtonPrimary onClick={onRetry}>
<i18n.Translate>retry</i18n.Translate>
</ButtonPrimary>
) : null}
<ButtonBoxDestructive onClick={onDelete}>
<i18n.Translate>&#x1F5D1;</i18n.Translate>
</ButtonBoxDestructive>
<ButtonDestructive onClick={onDelete}>
<i18n.Translate> Forget </i18n.Translate>
</ButtonDestructive>
</div>
</footer>
</WalletBox>
);
}
function amountToString(text: AmountLike) {
function amountToString(text: AmountLike): string {
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj);
return `${amount} ${aj.currency}`;
@ -140,23 +139,26 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Withdrawal</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
title="Total withdrawn"
text={amountToString(transaction.amountEffective)}
kind="positive"
/>
<Part
big
title="Chosen amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part title="Exchange fee" text={amountToString(fee)} kind="negative" />
<Part
big
title="Exchange fee"
text={amountToString(fee)}
kind="negative"
/>
<Part
title="Exchange"
text={new URL(transaction.exchangeBaseUrl).hostname}
@ -166,7 +168,9 @@ export function TransactionView({
);
}
const showLargePic = () => {};
const showLargePic = (): void => {
return;
};
if (transaction.type === TransactionType.Payment) {
const fee = Amounts.sub(
@ -177,11 +181,7 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Payment </h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
@ -241,11 +241,7 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Deposit </h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
@ -272,11 +268,7 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Refresh</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
@ -303,11 +295,7 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Tip</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
@ -334,11 +322,7 @@ export function TransactionView({
return (
<TransactionTemplate>
<h2>Refund</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
<br />
<Part
big
@ -391,5 +375,5 @@ export function TransactionView({
);
}
return <div></div>;
return <div />;
}

View File

@ -20,16 +20,15 @@
* @author Florian Dold
*/
import { JSX } from "preact/jsx-runtime";
import { Checkbox } from "../components/Checkbox";
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
import { Diagnostics } from "../components/Diagnostics";
import { WalletBox } from "../components/styled";
import { useDiagnostics } from "../hooks/useDiagnostics";
import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { h } from "preact";
import { h, VNode } from "preact";
export function WelcomePage() {
export function WelcomePage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const [diagnostics, timedOut] = useDiagnostics();
return (
@ -53,7 +52,7 @@ export function View({
togglePermissions,
diagnostics,
timedOut,
}: ViewProps): JSX.Element {
}: ViewProps): VNode {
return (
<WalletBox>
<h1>Browser Extension Installed!</h1>

View File

@ -2,11 +2,27 @@
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/static/style/pure.css" />
<link rel="stylesheet" type="text/css" href="/static/style/wallet.css" />
<link rel="stylesheet" type="text/css" href="/dist/popupEntryPoint.css" />
<link rel="icon" href="/static/img/icon.png" />
<script src="/dist/walletEntryPoint.js"></script>
<style>
html {
font-family: sans-serif; /* 1 */
}
h1 {
font-size: 2em;
}
input {
font: inherit;
}
body {
margin: 0;
font-size: 100%;
padding: 0;
background-color: #f8faf7;
font-family: Arial, Helvetica, sans-serif;
}
</style>
</head>
<body>