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"; import { URLSearchParams } from "./url.js";
interface PaytoUri { export type PaytoUri = PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank;
interface PaytoUriGeneric {
targetType: string; targetType: string;
targetPath: string; targetPath: string;
params: { [name: string]: 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://"; const paytoPfx = "payto://";
/** /**
@ -63,9 +82,33 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
params[v] = k; params[v] = k;
}); });
if (targetType === 'x-taler-bank') {
const parts = targetPath.split('/')
const host = parts[0]
const account = parts[1]
return { return {
targetPath, targetPath,
targetType, targetType,
params, 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. * Imports.
*/ */
import { i18n } from "@gnu-taler/taler-util"; 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 Match from "preact-router/match";
import { useDevContext } from "./context/devContext";
import { PopupNavigation } from "./components/styled"; import { PopupNavigation } from "./components/styled";
import { useDevContext } from "./context/devContext";
export enum Pages { export enum Pages {
welcome = "/welcome", welcome = "/welcome",
@ -59,7 +59,7 @@ interface TabProps {
children?: ComponentChildren; children?: ComponentChildren;
} }
function Tab(props: TabProps): JSX.Element { function Tab(props: TabProps): VNode {
let cssClass = ""; let cssClass = "";
if (props.current?.startsWith(props.target)) { if (props.current?.startsWith(props.target)) {
cssClass = "active"; cssClass = "active";

View File

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

View File

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

View File

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

View File

@ -15,8 +15,7 @@
*/ */
import { WalletDiagnostics } from "@gnu-taler/taler-util"; import { WalletDiagnostics } from "@gnu-taler/taler-util";
import { h } from "preact"; import { Fragment, h, VNode } from "preact";
import { JSX } from "preact/jsx-runtime";
import { PageLink } from "../renderHtml"; import { PageLink } from "../renderHtml";
interface Props { interface Props {
@ -24,18 +23,15 @@ interface Props {
diagnostics: WalletDiagnostics | undefined; diagnostics: WalletDiagnostics | undefined;
} }
export function Diagnostics({ export function Diagnostics({ timedOut, diagnostics }: Props): VNode {
timedOut,
diagnostics,
}: Props): JSX.Element | null {
if (timedOut) { if (timedOut) {
return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>; return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
} }
if (diagnostics) { if (diagnostics) {
if (diagnostics.errors.length === 0) { if (diagnostics.errors.length === 0) {
return null; return <Fragment />;
} else { }
return ( return (
<div <div
style={{ style={{
@ -69,7 +65,6 @@ export function Diagnostics({
</div> </div>
); );
} }
}
return <p>Running diagnostics ...</p>; 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/> 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 { useRef, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
interface Props { interface Props {
value: string; value: string;
@ -31,31 +30,35 @@ export function EditableText({
onChange, onChange,
label, label,
description, description,
}: Props): JSX.Element { }: Props): VNode {
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
let InputText; let InputText;
if (!editing) { if (!editing) {
InputText = () => ( InputText = function InputToEdit(): VNode {
return (
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<p>{value}</p> <p>{value}</p>
<button onClick={() => setEditing(true)}>edit</button> <button onClick={() => setEditing(true)}>edit</button>
</div> </div>
); );
};
} else { } else {
InputText = () => ( InputText = function InputEditing(): VNode {
return (
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<input value={value} ref={ref} type="text" id={`text-${name}`} /> <input value={value} ref={ref} type="text" id={`text-${name}`} />
<button <button
onClick={() => { onClick={() => {
if (ref.current) if (ref.current)
onChange(ref.current.value).then((r) => setEditing(false)); onChange(ref.current.value).then(() => setEditing(false));
}} }}
> >
confirm confirm
</button> </button>
</div> </div>
); );
};
} }
return ( return (
<div> <div>

View File

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

View File

@ -14,9 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { JSX } from "preact/jsx-runtime"; import { Fragment, h, VNode } from "preact";
import { NiceSelect } from "./styled/index"; import { NiceSelect } from "./styled/index";
import { h } from "preact";
interface Props { interface Props {
value?: string; value?: string;
@ -34,13 +33,12 @@ export function SelectList({
name, name,
value, value,
list, list,
canBeNull,
onChange, onChange,
label, label,
description, description,
}: Props): JSX.Element { }: Props): VNode {
return ( return (
<div> <Fragment>
<label <label
htmlFor={`text-${name}`} htmlFor={`text-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }} style={{ marginLeft: "0.5em", fontWeight: "bold" }}
@ -84,6 +82,6 @@ export function SelectList({
{description} {description}
</span> </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, Transaction,
TransactionType, TransactionType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { format, formatDistance } from "date-fns"; import { h, VNode } from "preact";
import { h } from "preact";
import imageBank from "../../static/img/ri-bank-line.svg"; import imageBank from "../../static/img/ri-bank-line.svg";
import imageHandHeart from "../../static/img/ri-hand-heart-line.svg"; import imageHandHeart from "../../static/img/ri-hand-heart-line.svg";
import imageRefresh from "../../static/img/ri-refresh-line.svg"; import imageRefresh from "../../static/img/ri-refresh-line.svg";
@ -36,11 +35,12 @@ import {
LargeText, LargeText,
LightText, LightText,
} from "./styled/index"; } from "./styled/index";
import { Time } from "./Time";
export function TransactionItem(props: { export function TransactionItem(props: {
tx: Transaction; tx: Transaction;
multiCurrency: boolean; multiCurrency: boolean;
}): JSX.Element { }): VNode {
const tx = props.tx; const tx = props.tx;
switch (tx.type) { switch (tx.type) {
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
@ -125,10 +125,7 @@ export function TransactionItem(props: {
} }
} }
function TransactionLayout(props: TransactionLayoutProps): JSX.Element { function TransactionLayout(props: TransactionLayoutProps): VNode {
const date = new Date(props.timestamp.t_ms);
const dateStr = format(date, "dd MMM, hh:mm");
return ( return (
<HistoryRow href={Pages.transaction.replace(":tid", props.id)}> <HistoryRow href={Pages.transaction.replace(":tid", props.id)}>
<img src={props.iconPath} /> <img src={props.iconPath} />
@ -146,7 +143,9 @@ function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
Waiting for confirmation Waiting for confirmation
</LightText> </LightText>
)} )}
<SmallLightText style={{ marginTop: 5 }}>{dateStr}</SmallLightText> <SmallLightText style={{ marginTop: 5 }}>
<Time timestamp={props.timestamp} format="dd MMM, hh:mm" />
</SmallLightText>
</Column> </Column>
<TransactionAmount <TransactionAmount
pending={props.pending} pending={props.pending}
@ -177,7 +176,7 @@ interface TransactionAmountProps {
multiCurrency: boolean; multiCurrency: boolean;
} }
function TransactionAmount(props: TransactionAmountProps): JSX.Element { function TransactionAmount(props: TransactionAmountProps): VNode {
const [currency, amount] = props.amount.split(":"); const [currency, amount] = props.amount.split(":");
let sign: string; let sign: string;
switch (props.debitCreditIndicator) { switch (props.debitCreditIndicator) {

View File

@ -85,7 +85,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
overflow: auto; overflow: auto;
table td { table td {
padding: 5px 10px; padding: 5px 5px;
} }
table tr { table tr {
border-bottom: 1px solid black; border-bottom: 1px solid black;
@ -328,7 +328,8 @@ const ButtonVariant = styled(Button)`
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 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); background-color: rgb(66, 184, 221);
`; `;
export const ButtonBoxPrimary = styled(ButtonBox)` export const ButtonBoxPrimary = styled(ButtonBox)`
@ -501,29 +502,40 @@ export const Input = styled.div<{ invalid?: boolean }>`
`; `;
export const InputWithLabel = styled.div<{ invalid?: boolean }>` export const InputWithLabel = styled.div<{ invalid?: boolean }>`
/* display: flex; */
& label { & label {
display: block; display: block;
font-weight: bold;
margin-left: 0.5em;
padding: 5px; padding: 5px;
color: ${({ invalid }) => (!invalid ? "inherit" : "red")}; color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
} }
& > div {
position: relative; & div {
line-height: 24px;
display: flex; display: flex;
top: 0px; }
bottom: 0px; & div > span {
& > div {
position: absolute;
background-color: lightgray; background-color: lightgray;
padding: 5px; box-sizing: border-box;
margin: 2px; border-bottom-left-radius: 0.25em;
} border-top-left-radius: 0.25em;
height: 2em;
& > input { display: inline-block;
flex: 1; padding-left: 0.5em;
padding: 5px; padding-right: 0.5em;
border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")}; 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; flex-direction: column;
/* margin: 0.5em; */ /* margin: 0.5em; */
padding: 1em; padding: 1em;
margin: 1em;
/* width: 100%; */ /* width: 100%; */
color: #721c24; color: #721c24;
background: #f8d7da; 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` export const NiceSelect = styled.div`
& > select { & > select {
-webkit-appearance: none; -webkit-appearance: none;
@ -600,11 +615,18 @@ export const NiceSelect = styled.div`
appearance: none; appearance: none;
outline: 0; outline: 0;
box-shadow: none; 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; background-color: white;
flex: 1; border-radius: 0.25rem;
padding: 0.5em 1em; font-size: 1em;
padding: 0.5em 3em 0.5em 1em;
cursor: pointer; cursor: pointer;
} }
@ -613,27 +635,6 @@ export const NiceSelect = styled.div`
/* width: 10em; */ /* width: 10em; */
overflow: hidden; overflow: hidden;
border-radius: 0.25em; 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` export const Outlined = styled.div`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,9 +20,10 @@
*/ */
import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { VNode, h } from "preact"; import { h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage"; import { ErrorMessage } from "../components/ErrorMessage";
import { SelectList } from "../components/SelectList";
import { import {
ButtonPrimary, ButtonPrimary,
Input, Input,
@ -33,32 +34,56 @@ import {
export interface Props { export interface Props {
error: string | undefined; error: string | undefined;
currency: string | undefined;
initialExchange?: string;
initialAmount?: string; initialAmount?: string;
onExchangeChange: (exchange: string) => void; exchangeList: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>; onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
} }
export function CreateManualWithdraw({ export function CreateManualWithdraw({
onExchangeChange,
initialExchange,
initialAmount, initialAmount,
exchangeList,
error, error,
currency,
onCreate, onCreate,
}: Props): VNode { }: 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 [exchange, setExchange] = useState(initialExchange || "");
const [currency, setCurrency] = useState(exchangeList[initialExchange] ?? "");
const [amount, setAmount] = useState(initialAmount || ""); const [amount, setAmount] = useState(initialAmount || "");
const parsedAmount = Amounts.parse(`${currency}:${amount}`); const parsedAmount = Amounts.parse(`${currency}:${amount}`);
let timeout = useRef<number | undefined>(undefined); function changeExchange(exchange: string): void {
useEffect(() => { setExchange(exchange);
if (timeout) window.clearTimeout(timeout.current); setCurrency(exchangeList[exchange]);
timeout.current = window.setTimeout(async () => { }
onExchangeChange(exchange);
}, 1000); function changeCurrency(currency: string): void {
}, [exchange]); 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 ( return (
<WalletBox> <WalletBox>
@ -73,26 +98,38 @@ export function CreateManualWithdraw({
withdraw the coins withdraw the coins
</LightText> </LightText>
<p> <p>
<Input invalid={!!exchange && !currency}> <Input>
<label>Exchange</label> <SelectList
<input label="Currency"
type="text" list={currencyMap}
placeholder="https://" name="currency"
value={exchange} value={currency}
onChange={(e) => setExchange(e.currentTarget.value)} onChange={changeCurrency}
/> />
<small>http://exchange.taler:8081</small>
</Input> </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 && ( {currency && (
<InputWithLabel invalid={!!amount && !parsedAmount}> <InputWithLabel invalid={!!amount && !parsedAmount}>
<label>Amount</label> <label>Amount</label>
<div> <div>
<div>{currency}</div> <span>{currency}</span>
<input <input
type="number" type="number"
style={{ paddingLeft: `${currency.length}em` }}
value={amount} value={amount}
onChange={(e) => setAmount(e.currentTarget.value)} onInput={(e) => setAmount(e.currentTarget.value)}
/> />
</div> </div>
</InputWithLabel> </InputWithLabel>
@ -105,7 +142,7 @@ export function CreateManualWithdraw({
disabled={!parsedAmount || !exchange} disabled={!parsedAmount || !exchange}
onClick={() => onCreate(exchange, parsedAmount!)} onClick={() => onCreate(exchange, parsedAmount!)}
> >
Create Start withdrawal
</ButtonPrimary> </ButtonPrimary>
</footer> </footer>
</WalletBox> </WalletBox>

View File

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

View File

@ -26,44 +26,31 @@ import {
import { ReserveCreated } from "./ReserveCreated.js"; import { ReserveCreated } from "./ReserveCreated.js";
import { route } from "preact-router"; import { route } from "preact-router";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
interface Props {} export function ManualWithdrawPage(): VNode {
export function ManualWithdrawPage({}: Props): VNode {
const [success, setSuccess] = useState< const [success, setSuccess] = useState<
AcceptManualWithdrawalResult | undefined | {
response: AcceptManualWithdrawalResult;
exchangeBaseUrl: string;
amount: AmountJson;
}
| undefined
>(undefined); >(undefined);
const [currency, setCurrency] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
async function onExchangeChange(exchange: string | undefined): Promise<void> { const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
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);
}
}
async function doCreate( async function doCreate(
exchangeBaseUrl: string, exchangeBaseUrl: string,
amount: AmountJson, amount: AmountJson,
): Promise<void> { ): Promise<void> {
try { try {
const resp = await wxApi.acceptManualWithdrawal( const response = await wxApi.acceptManualWithdrawal(
exchangeBaseUrl, exchangeBaseUrl,
Amounts.stringify(amount), Amounts.stringify(amount),
); );
setSuccess(resp); setSuccess({ exchangeBaseUrl, response, amount });
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
setError(e.message); setError(e.message);
@ -77,8 +64,10 @@ export function ManualWithdrawPage({}: Props): VNode {
if (success) { if (success) {
return ( return (
<ReserveCreated <ReserveCreated
reservePub={success.reservePub} reservePub={success.response.reservePub}
paytos={success.exchangePaytoUris} payto={success.response.exchangePaytoUris[0]}
exchangeBaseUrl={success.exchangeBaseUrl}
amount={success.amount}
onBack={() => { onBack={() => {
route(Pages.balance); 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 ( return (
<CreateManualWithdraw <CreateManualWithdraw
error={error} error={error}
currency={currency} exchangeList={exchangeList}
onCreate={doCreate} onCreate={doCreate}
onExchangeChange={onExchangeChange}
/> />
); );
} }

View File

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

View File

@ -28,10 +28,26 @@ export default {
argTypes: {}, argTypes: {},
}; };
export const InitialState = createExample(TestedComponent, { export const TalerBank = createExample(TestedComponent, {
reservePub: "ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ", reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
paytos: [ payto:
"payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", "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 {
import { useState } from "preact/hooks"; 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 { QR } from "../components/QR";
import { ButtonBox, FontIcon, WalletBox } from "../components/styled"; import {
ButtonDestructive,
ButtonPrimary,
WalletBox,
WarningBox,
} from "../components/styled";
export interface Props { export interface Props {
reservePub: string; reservePub: string;
paytos: string[]; payto: string;
exchangeBaseUrl: string;
amount: AmountJson;
onBack: () => void; onBack: () => void;
} }
export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode { interface BankDetailsProps {
const [opened, setOpened] = useState(-1); 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 ( return (
<WalletBox> <WalletBox>
<section> <section>
<h2>Reserve created!</h2> <h1>Bank transfer details</h1>
<p> <p>
Now you need to send money to the exchange to one of the following Please wire <b>{Amounts.stringify(amount)}</b> to:
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.
</p> </p>
<BankDetailsByPaytoType
amount={Amounts.stringify(amount)}
exchangeBaseUrl={exchangeBaseUrl}
payto={paytoURI}
subject={reservePub}
/>
</section> </section>
<section> <section>
<ul>
{paytos.map((href, idx) => {
const url = new URL(href);
return (
<li key={idx}>
<p> <p>
<a <WarningBox>
href="" Make sure to use the correct subject, otherwise the money will not
onClick={(e) => { arrive in this wallet.
setOpened((o) => (o === idx ? -1 : idx)); </WarningBox>
e.preventDefault(); </p>
}}
>
{url.pathname}
</a>
{opened === idx && (
<Fragment>
<p> <p>
If your system supports RFC 8905, you can do this by Alternative, you can also scan this QR code or open{" "}
opening <a href={href}>this URI</a> or scan the QR with <a href={payto}>this link</a> if you have a banking app installed that
your wallet supports RFC 8905
</p> </p>
<QR text={href} /> <QR text={payto} />
</Fragment>
)}
</p>
</li>
);
})}
</ul>
</section> </section>
<footer> <footer>
<ButtonBox onClick={onBack}>
<FontIcon>&#x2190;</FontIcon>
</ButtonBox>
<div /> <div />
<ButtonDestructive onClick={onBack}>Cancel withdraw</ButtonDestructive>
</footer> </footer>
</WalletBox> </WalletBox>
); );

View File

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

View File

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

View File

@ -2,11 +2,27 @@
<html> <html>
<head> <head>
<meta charset="utf-8" /> <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="stylesheet" type="text/css" href="/dist/popupEntryPoint.css" />
<link rel="icon" href="/static/img/icon.png" /> <link rel="icon" href="/static/img/icon.png" />
<script src="/dist/walletEntryPoint.js"></script> <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> </head>
<body> <body>