This commit is contained in:
Sebastian 2021-11-15 11:18:58 -03:00
parent 9692f589c6
commit 1d4815c66c
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
86 changed files with 5048 additions and 3658 deletions

View File

@ -13,6 +13,7 @@
"compile": "tsc && rollup -c",
"build-storybook": "build-storybook",
"storybook": "start-storybook -s . -p 6006",
"pretty": "prettier --write src",
"watch": "tsc --watch & rollup -w -c"
},
"dependencies": {
@ -80,4 +81,4 @@
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$": "<rootDir>/tests/__mocks__/fileTransformer.js"
}
}
}
}

View File

@ -28,29 +28,29 @@ import { i18n } from "@gnu-taler/taler-util";
import { ComponentChildren, JSX, h } from "preact";
import Match from "preact-router/match";
import { useDevContext } from "./context/devContext";
import { PopupNavigation } from './components/styled'
import { PopupNavigation } from "./components/styled";
export enum Pages {
welcome = '/welcome',
balance = '/balance',
manual_withdraw = '/manual-withdraw',
settings = '/settings',
dev = '/dev',
cta = '/cta',
backup = '/backup',
history = '/history',
transaction = '/transaction/:tid',
provider_detail = '/provider/:pid',
provider_add = '/provider/add',
welcome = "/welcome",
balance = "/balance",
manual_withdraw = "/manual-withdraw",
settings = "/settings",
dev = "/dev",
cta = "/cta",
backup = "/backup",
history = "/history",
transaction = "/transaction/:tid",
provider_detail = "/provider/:pid",
provider_add = "/provider/add",
reset_required = '/reset-required',
payback = '/payback',
return_coins = '/return-coins',
reset_required = "/reset-required",
payback = "/payback",
return_coins = "/return-coins",
pay = '/pay',
refund = '/refund',
tips = '/tip',
withdraw = '/withdraw',
pay = "/pay",
refund = "/refund",
tips = "/tip",
withdraw = "/withdraw",
}
interface TabProps {
@ -71,23 +71,28 @@ function Tab(props: TabProps): JSX.Element {
);
}
export function NavBar({ devMode, path }: { path: string, devMode: boolean }) {
return <PopupNavigation devMode={devMode}>
<div>
<Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
<Tab target="/history" current={path}>{i18n.str`History`}</Tab>
<Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab>
<Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab>
{devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>}
</div>
</PopupNavigation>
export function NavBar({ devMode, path }: { path: string; devMode: boolean }) {
return (
<PopupNavigation devMode={devMode}>
<div>
<Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
<Tab target="/history" current={path}>{i18n.str`History`}</Tab>
<Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab>
<Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab>
{devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>}
</div>
</PopupNavigation>
);
}
export function WalletNavBar() {
const { devMode } = useDevContext()
return <Match>{({ path }: any) => {
console.log("path", path)
return <NavBar devMode={devMode} path={path} />
}}</Match>
const { devMode } = useDevContext();
return (
<Match>
{({ path }: any) => {
console.log("path", path);
return <NavBar devMode={devMode} path={path} />;
}}
</Match>
);
}

View File

@ -21,24 +21,23 @@ exports.BrowserCryptoWorkerFactory = void 0;
* @author Florian Dold
*/
class BrowserCryptoWorkerFactory {
startWorker() {
const workerCtor = Worker;
const workerPath = "/browserWorkerEntry.js";
return new workerCtor(workerPath);
}
getConcurrency() {
let concurrency = 2;
try {
// only works in the browser
// tslint:disable-next-line:no-string-literal
concurrency = navigator["hardwareConcurrency"];
concurrency = Math.max(1, Math.ceil(concurrency / 2));
}
catch (e) {
concurrency = 2;
}
return concurrency;
startWorker() {
const workerCtor = Worker;
const workerPath = "/browserWorkerEntry.js";
return new workerCtor(workerPath);
}
getConcurrency() {
let concurrency = 2;
try {
// only works in the browser
// tslint:disable-next-line:no-string-literal
concurrency = navigator["hardwareConcurrency"];
concurrency = Math.max(1, Math.ceil(concurrency / 2));
} catch (e) {
concurrency = 2;
}
return concurrency;
}
}
exports.BrowserCryptoWorkerFactory = BrowserCryptoWorkerFactory;
//# sourceMappingURL=browserCryptoWorkerFactory.js.map
//# sourceMappingURL=browserCryptoWorkerFactory.js.map

View File

@ -19,7 +19,10 @@
* @author Florian Dold
*/
import type { CryptoWorker, CryptoWorkerFactory } from "@gnu-taler/taler-wallet-core";
import type {
CryptoWorker,
CryptoWorkerFactory,
} from "@gnu-taler/taler-wallet-core";
export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {
startWorker(): CryptoWorker {

View File

@ -21,41 +21,44 @@ exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;
* WebExtension APIs consistently.
*/
function isFirefox() {
const rt = chrome.runtime;
if (typeof rt.getBrowserInfo === "function") {
return true;
}
return false;
const rt = chrome.runtime;
if (typeof rt.getBrowserInfo === "function") {
return true;
}
return false;
}
exports.isFirefox = isFirefox;
/**
* Check if we are running under nodejs.
*/
function isNode() {
return typeof process !== "undefined" && process.release.name === "node";
return typeof process !== "undefined" && process.release.name === "node";
}
exports.isNode = isNode;
function getPermissionsApi() {
const myBrowser = globalThis.browser;
if (typeof myBrowser === "object" &&
typeof myBrowser.permissions === "object") {
return {
addPermissionsListener: () => {
// Not supported yet.
},
contains: myBrowser.permissions.contains,
request: myBrowser.permissions.request,
remove: myBrowser.permissions.remove,
};
}
else {
return {
addPermissionsListener: chrome.permissions.onAdded.addListener.bind(chrome.permissions.onAdded),
contains: chrome.permissions.contains,
request: chrome.permissions.request,
remove: chrome.permissions.remove,
};
}
const myBrowser = globalThis.browser;
if (
typeof myBrowser === "object" &&
typeof myBrowser.permissions === "object"
) {
return {
addPermissionsListener: () => {
// Not supported yet.
},
contains: myBrowser.permissions.contains,
request: myBrowser.permissions.request,
remove: myBrowser.permissions.remove,
};
} else {
return {
addPermissionsListener: chrome.permissions.onAdded.addListener.bind(
chrome.permissions.onAdded,
),
contains: chrome.permissions.contains,
request: chrome.permissions.request,
remove: chrome.permissions.remove,
};
}
}
exports.getPermissionsApi = getPermissionsApi;
//# sourceMappingURL=compat.js.map
//# sourceMappingURL=compat.js.map

View File

@ -24,7 +24,13 @@ interface Props {
name: string;
description?: string;
}
export function Checkbox({ name, enabled, onToggle, label, description }: Props): JSX.Element {
export function Checkbox({
name,
enabled,
onToggle,
label,
description,
}: Props): JSX.Element {
return (
<div>
<input
@ -32,23 +38,26 @@ export function Checkbox({ name, enabled, onToggle, label, description }: Props)
onClick={onToggle}
type="checkbox"
id={`checkbox-${name}`}
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
/>
<label
htmlFor={`checkbox-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
>
{label}
</label>
{description && <span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>}
{description && (
<span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>
)}
</div>
);
}

View File

@ -16,7 +16,7 @@
import { JSX } from "preact/jsx-runtime";
import { Outlined, StyledCheckboxLabel } from "./styled/index";
import { h } from 'preact';
import { h } from "preact";
interface Props {
enabled: boolean;
@ -25,28 +25,39 @@ interface Props {
name: string;
}
const Tick = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
style={{ backgroundColor: "green" }}
>
<path
fill="none"
stroke="white"
stroke-width="3"
d="M1.73 12.91l6.37 6.37L22.79 4.59"
/>
</svg>
);
const Tick = () => <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
style={{ backgroundColor: 'green' }}
>
<path
fill="none"
stroke="white"
stroke-width="3"
d="M1.73 12.91l6.37 6.37L22.79 4.59"
/>
</svg>
export function CheckboxOutlined({ name, enabled, onToggle, label }: Props): JSX.Element {
export function CheckboxOutlined({
name,
enabled,
onToggle,
label,
}: Props): JSX.Element {
return (
<Outlined>
<StyledCheckboxLabel onClick={onToggle}>
<span>
<input type="checkbox" name={name} checked={enabled} disabled={false} />
<input
type="checkbox"
name={name}
checked={enabled}
disabled={false}
/>
<div>
<Tick />
</div>

View File

@ -14,9 +14,15 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { JSX, h } from "preact";
import { JSX, h } from "preact";
export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggle: () => void; }): JSX.Element {
export function DebugCheckbox({
enabled,
onToggle,
}: {
enabled: boolean;
onToggle: () => void;
}): JSX.Element {
return (
<div>
<input
@ -24,7 +30,8 @@ export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggl
onClick={onToggle}
type="checkbox"
id="checkbox-perm"
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
/>
<label
htmlFor="checkbox-perm"
style={{ marginLeft: "0.5em", fontWeight: "bold" }}

View File

@ -21,11 +21,13 @@ import { PageLink } from "../renderHtml";
interface Props {
timedOut: boolean;
diagnostics: WalletDiagnostics | undefined
diagnostics: WalletDiagnostics | undefined;
}
export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null {
export function Diagnostics({
timedOut,
diagnostics,
}: Props): JSX.Element | null {
if (timedOut) {
return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
}
@ -60,8 +62,8 @@ export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null
<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.
<PageLink pageName="/reset-required">here</PageLink> to reset the
wallet database.
</p>
) : null}
</div>

View File

@ -25,25 +25,37 @@ interface Props {
name: string;
description?: string;
}
export function EditableText({ name, value, onChange, label, description }: Props): JSX.Element {
const [editing, setEditing] = useState(false)
const ref = useRef<HTMLInputElement>(null)
export function EditableText({
name,
value,
onChange,
label,
description,
}: Props): JSX.Element {
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 = () => (
<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 = () => (
<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>
);
}
return (
<div>
@ -54,16 +66,18 @@ export function EditableText({ name, value, onChange, label, description }: Prop
{label}
</label>
<InputText />
{description && <span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>}
{description && (
<span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>
)}
</div>
);
}

View File

@ -13,22 +13,35 @@
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 { VNode, h } from "preact";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import arrowDown from '../../static/img/chevron-down.svg';
import arrowDown from "../../static/img/chevron-down.svg";
import { ErrorBox } from "./styled";
export function ErrorMessage({ title, description }: { title?: string|VNode; description?: string; }) {
export function ErrorMessage({
title,
description,
}: {
title?: string | VNode;
description?: string;
}) {
const [showErrorDetail, setShowErrorDetail] = useState(false);
if (!title)
return null;
return <ErrorBox style={{paddingTop: 0, paddingBottom: 0}}>
<div>
<p>{title}</p>
{ description && <button onClick={() => { setShowErrorDetail(v => !v); }}>
<img style={{ height: '1.5em' }} src={arrowDown} />
</button> }
</div>
{showErrorDetail && <p>{description}</p>}
</ErrorBox>;
if (!title) return null;
return (
<ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}>
<div>
<p>{title}</p>
{description && (
<button
onClick={() => {
setShowErrorDetail((v) => !v);
}}
>
<img style={{ height: "1.5em" }} src={arrowDown} />
</button>
)}
</div>
{showErrorDetail && <p>{description}</p>}
</ErrorBox>
);
}

View File

@ -13,66 +13,80 @@
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 { useState } from "preact/hooks"
import { JSXInternal } from "preact/src/jsx"
import { h } from 'preact';
import { Fragment, VNode } from "preact";
import { useState } from "preact/hooks";
import { JSXInternal } from "preact/src/jsx";
import { h } from "preact";
export function ExchangeXmlTos({ doc }: { doc: Document }) {
const termsNode = doc.querySelector('[ids=terms-of-service]')
const termsNode = doc.querySelector("[ids=terms-of-service]");
if (!termsNode) {
return <div>
<p>The exchange send us an xml but there is no node with 'ids=terms-of-service'. This is the content:</p>
<pre>{new XMLSerializer().serializeToString(doc)}</pre>
</div>
return (
<div>
<p>
The exchange send us an xml but there is no node with
'ids=terms-of-service'. This is the content:
</p>
<pre>{new XMLSerializer().serializeToString(doc)}</pre>
</div>
);
}
return <Fragment>
{Array.from(termsNode.children).map(renderChild)}
</Fragment>
return <Fragment>{Array.from(termsNode.children).map(renderChild)}</Fragment>;
}
/**
* Map XML elements into HTML
* @param child
* @returns
* @param child
* @returns
*/
function renderChild(child: Element): VNode {
const children = Array.from(child.children)
const children = Array.from(child.children);
switch (child.nodeName) {
case 'title': return <header>{child.textContent}</header>
case '#text': return <Fragment />
case 'paragraph': return <p>{child.textContent}</p>
case 'section': {
return <AnchorWithOpenState href={`#terms-${child.getAttribute('ids')}`}>
{children.map(renderChild)}
</AnchorWithOpenState>
case "title":
return <header>{child.textContent}</header>;
case "#text":
return <Fragment />;
case "paragraph":
return <p>{child.textContent}</p>;
case "section": {
return (
<AnchorWithOpenState href={`#terms-${child.getAttribute("ids")}`}>
{children.map(renderChild)}
</AnchorWithOpenState>
);
}
case 'bullet_list': {
return <ul>{children.map(renderChild)}</ul>
case "bullet_list": {
return <ul>{children.map(renderChild)}</ul>;
}
case 'enumerated_list': {
return <ol>{children.map(renderChild)}</ol>
case "enumerated_list": {
return <ol>{children.map(renderChild)}</ol>;
}
case 'list_item': {
return <li>{children.map(renderChild)}</li>
case "list_item": {
return <li>{children.map(renderChild)}</li>;
}
case 'block_quote': {
return <div>{children.map(renderChild)}</div>
case "block_quote": {
return <div>{children.map(renderChild)}</div>;
}
default: return <div style={{ color: 'red', display: 'hidden' }}>unknown tag {child.nodeName} <a></a></div>
default:
return (
<div style={{ color: "red", display: "hidden" }}>
unknown tag {child.nodeName} <a></a>
</div>
);
}
}
/**
* Simple anchor with a state persisted into 'data-open' prop
* @returns
* @returns
*/
function AnchorWithOpenState(props: JSXInternal.HTMLAttributes<HTMLAnchorElement>) {
const [open, setOpen] = useState<boolean>(false)
function AnchorWithOpenState(
props: JSXInternal.HTMLAttributes<HTMLAnchorElement>,
) {
const [open, setOpen] = useState<boolean>(false);
function doClick(e: JSXInternal.TargetedMouseEvent<HTMLAnchorElement>) {
setOpen(!open);
e.preventDefault();
}
return <a data-open={open ? 'true' : 'false'} onClick={doClick} {...props} />
return <a data-open={open ? "true" : "false"} onClick={doClick} {...props} />;
}

View File

@ -17,15 +17,22 @@
import { h } from "preact";
export function LogoHeader() {
return <div style={{
display: 'flex',
justifyContent: 'space-around',
margin: '2em',
}}>
<img style={{
width: 150,
height: 70,
}} src="/static/img/logo-2021.svg" width="150" />
</div>
}
return (
<div
style={{
display: "flex",
justifyContent: "space-around",
margin: "2em",
}}
>
<img
style={{
width: 150,
height: 70,
}}
src="/static/img/logo-2021.svg"
width="150"
/>
</div>
);
}

View File

@ -15,18 +15,28 @@
*/
import { AmountLike } from "@gnu-taler/taler-util";
import { ExtraLargeText, LargeText, SmallLightText } from "./styled";
import { h } from 'preact';
import { h } from "preact";
export type Kind = 'positive' | 'negative' | 'neutral';
export type Kind = "positive" | "negative" | "neutral";
interface Props {
title: string, text: AmountLike, kind: Kind, big?: boolean
title: string;
text: AmountLike;
kind: Kind;
big?: boolean;
}
export function Part({ text, title, kind, big }: Props) {
const Text = big ? ExtraLargeText : LargeText;
return <div style={{ margin: '1em' }}>
<SmallLightText style={{ margin: '.5em' }}>{title}</SmallLightText>
<Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}>
{text}
</Text>
</div>
return (
<div style={{ margin: "1em" }}>
<SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText>
<Text
style={{
color:
kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
}}
>
{text}
</Text>
</div>
);
}

View File

@ -14,24 +14,35 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h, VNode } from "preact";
import { useEffect, useRef } from "preact/hooks";
import qrcode from "qrcode-generator";
export function QR({ text }: { text: string; }):VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!divRef.current) return
const qr = qrcode(0, 'L');
qr.addData(text);
qr.make();
divRef.current.innerHTML = qr.createSvgTag({
scalable: true,
});
});
return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
</div>;
}
import { h, VNode } from "preact";
import { useEffect, useRef } from "preact/hooks";
import qrcode from "qrcode-generator";
export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!divRef.current) return;
const qr = qrcode(0, "L");
qr.addData(text);
qr.make();
divRef.current.innerHTML = qr.createSvgTag({
scalable: true,
});
});
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
ref={divRef}
/>
</div>
);
}

View File

@ -23,46 +23,67 @@ interface Props {
onChange: (s: string) => void;
label: string;
list: {
[label: string]: string
}
[label: string]: string;
};
name: string;
description?: string;
canBeNull?: boolean;
}
export function SelectList({ name, value, list, canBeNull, onChange, label, description }: Props): JSX.Element {
return <div>
<label
htmlFor={`text-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
> {label}</label>
<NiceSelect>
<select name={name} onChange={(e) => {
console.log(e.currentTarget.value, value)
onChange(e.currentTarget.value)
}}>
{value !== undefined ? <option selected>
{list[value]}
</option> : <option selected disabled>
Select one option
</option>}
{Object.keys(list)
.filter((l) => l !== value)
.map(key => <option value={key} key={key}>{list[key]}</option>)
}
</select>
</NiceSelect>
{description && <span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>}
</div>
export function SelectList({
name,
value,
list,
canBeNull,
onChange,
label,
description,
}: Props): JSX.Element {
return (
<div>
<label
htmlFor={`text-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
>
{" "}
{label}
</label>
<NiceSelect>
<select
name={name}
onChange={(e) => {
console.log(e.currentTarget.value, value);
onChange(e.currentTarget.value);
}}
>
{value !== undefined ? (
<option selected>{list[value]}</option>
) : (
<option selected disabled>
Select one option
</option>
)}
{Object.keys(list)
.filter((l) => l !== value)
.map((key) => (
<option value={key} key={key}>
{list[key]}
</option>
))}
</select>
</NiceSelect>
{description && (
<span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>
)}
</div>
);
}

View File

@ -14,18 +14,33 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountString, Timestamp, Transaction, TransactionType } from '@gnu-taler/taler-util';
import { format, formatDistance } from 'date-fns';
import { h } 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';
import imageRefund from '../../static/img/ri-refund-2-line.svg';
import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg';
import {
AmountString,
Timestamp,
Transaction,
TransactionType,
} from "@gnu-taler/taler-util";
import { format, formatDistance } from "date-fns";
import { h } 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";
import imageRefund from "../../static/img/ri-refund-2-line.svg";
import imageShoppingCart from "../../static/img/ri-shopping-cart-line.svg";
import { Pages } from "../NavigationBar";
import { Column, ExtraLargeText, HistoryRow, SmallLightText, LargeText, LightText } from './styled/index';
import {
Column,
ExtraLargeText,
HistoryRow,
SmallLightText,
LargeText,
LightText,
} from "./styled/index";
export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean }): JSX.Element {
export function TransactionItem(props: {
tx: Transaction;
multiCurrency: boolean;
}): JSX.Element {
const tx = props.tx;
switch (tx.type) {
case TransactionType.Withdrawal:
@ -112,20 +127,26 @@ export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean
function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
const date = new Date(props.timestamp.t_ms);
const dateStr = format(date, 'dd MMM, hh:mm')
const dateStr = format(date, "dd MMM, hh:mm");
return (
<HistoryRow href={Pages.transaction.replace(':tid', props.id)}>
<HistoryRow href={Pages.transaction.replace(":tid", props.id)}>
<img src={props.iconPath} />
<Column>
<LargeText>
<div>{props.title}</div>
{props.subtitle && <div style={{color:'gray', fontSize:'medium', marginTop: 5}}>{props.subtitle}</div>}
{props.subtitle && (
<div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
{props.subtitle}
</div>
)}
</LargeText>
{props.pending &&
<LightText style={{ marginTop: 5, marginBottom: 5 }}>Waiting for confirmation</LightText>
}
<SmallLightText style={{marginTop:5 }}>{dateStr}</SmallLightText>
{props.pending && (
<LightText style={{ marginTop: 5, marginBottom: 5 }}>
Waiting for confirmation
</LightText>
)}
<SmallLightText style={{ marginTop: 5 }}>{dateStr}</SmallLightText>
</Column>
<TransactionAmount
pending={props.pending}
@ -170,14 +191,18 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {
sign = "";
}
return (
<Column style={{
textAlign: 'center',
color:
props.pending ? "gray" :
(sign === '+' ? 'darkgreen' :
(sign === '-' ? 'darkred' :
undefined))
}}>
<Column
style={{
textAlign: "center",
color: props.pending
? "gray"
: sign === "+"
? "darkgreen"
: sign === "-"
? "darkred"
: undefined,
}}
>
<ExtraLargeText>
{sign}
{amount}
@ -187,4 +212,3 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {
</Column>
);
}

View File

@ -14,18 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
// need to import linaria types, otherwise compiler will complain
import type * as Linaria from '@linaria/core';
import type * as Linaria from "@linaria/core";
import { styled } from '@linaria/react';
import { styled } from "@linaria/react";
export const PaymentStatus = styled.div<{ color: string }>`
padding: 5px;
border-radius: 5px;
color: white;
background-color: ${p => p.color};
`
background-color: ${(p) => p.color};
`;
export const WalletAction = styled.div`
display: flex;
@ -36,9 +35,9 @@ export const WalletAction = styled.div`
margin: auto;
height: 100%;
& h1:first-child {
margin-top: 0;
margin-top: 0;
}
section {
margin-bottom: 2em;
@ -47,7 +46,7 @@ export const WalletAction = styled.div`
margin-left: 8px;
}
}
`
`;
export const WalletActionOld = styled.section`
border: solid 5px black;
border-radius: 10px;
@ -59,17 +58,17 @@ export const WalletActionOld = styled.section`
margin: auto;
height: 100%;
& h1:first-child {
margin-top: 0;
margin-top: 0;
}
`
`;
export const DateSeparator = styled.div`
color: gray;
margin: .2em;
margin: 0.2em;
margin-top: 1em;
`
`;
export const WalletBox = styled.div<{ noPadding?: boolean }>`
display: flex;
flex-direction: column;
@ -79,10 +78,10 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
width: 400px;
}
& > section {
padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'};
padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'};
padding-left: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
padding-right: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
// this margin will send the section up when used with a header
margin-bottom: auto;
margin-bottom: auto;
overflow: auto;
table td {
@ -128,13 +127,13 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
margin-left: 8px;
}
}
`
`;
export const Middle = styled.div`
justify-content: space-around;
display: flex;
flex-direction: column;
height: 100%;
`
justify-content: space-around;
display: flex;
flex-direction: column;
height: 100%;
`;
export const PopupBox = styled.div<{ noPadding?: boolean }>`
height: 290px;
@ -144,9 +143,9 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
justify-content: space-between;
& > section {
padding: ${({ noPadding }) => noPadding ? '0px' : '8px'};
padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
// this margin will send the section up when used with a header
margin-bottom: auto;
margin-bottom: auto;
overflow-y: auto;
table td {
@ -201,8 +200,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
margin-left: 8px;
}
}
`
`;
export const Button = styled.button<{ upperCased?: boolean }>`
display: inline-block;
@ -214,7 +212,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
font-family: inherit;
font-size: 100%;
@ -223,7 +221,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
color: rgba(0, 0, 0, 0.8); /* rgba supported */
border: 1px solid #999; /*IE 6/7/8*/
border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
background-color: '#e6e6e6';
background-color: "#e6e6e6";
text-decoration: none;
border-radius: 2px;
@ -263,7 +261,7 @@ export const Link = styled.a<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
font-family: inherit;
font-size: 100%;
@ -304,9 +302,9 @@ export const FontIcon = styled.div`
text-align: center;
font-weight: bold;
/* vertical-align: text-top; */
`
`;
export const ButtonBox = styled(Button)`
padding: .5em;
padding: 0.5em;
width: fit-content;
height: 2em;
@ -322,89 +320,87 @@ export const ButtonBox = styled(Button)`
border-radius: 4px;
border-color: black;
color: black;
`
`;
const ButtonVariant = styled(Button)`
color: white;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
`
`;
export const ButtonPrimary = styled(ButtonVariant)`
background-color: rgb(66, 184, 221);
`
`;
export const ButtonBoxPrimary = styled(ButtonBox)`
color: rgb(66, 184, 221);
border-color: rgb(66, 184, 221);
`
`;
export const ButtonSuccess = styled(ButtonVariant)`
background-color: #388e3c;
`
`;
export const LinkSuccess = styled(Link)`
color: #388e3c;
`
`;
export const ButtonBoxSuccess = styled(ButtonBox)`
color: #388e3c;
border-color: #388e3c;
`
`;
export const ButtonWarning = styled(ButtonVariant)`
background-color: rgb(223, 117, 20);
`
`;
export const LinkWarning = styled(Link)`
color: rgb(223, 117, 20);
`
`;
export const ButtonBoxWarning = styled(ButtonBox)`
color: rgb(223, 117, 20);
border-color: rgb(223, 117, 20);
`
`;
export const ButtonDestructive = styled(ButtonVariant)`
background-color: rgb(202, 60, 60);
`
`;
export const ButtonBoxDestructive = styled(ButtonBox)`
color: rgb(202, 60, 60);
border-color: rgb(202, 60, 60);
`
`;
export const BoldLight = styled.div`
color: gray;
font-weight: bold;
`
color: gray;
font-weight: bold;
`;
export const Centered = styled.div`
text-align: center;
& > :not(:first-child) {
margin-top: 15px;
}
`
`;
export const Row = styled.div`
display: flex;
margin: 0.5em 0;
justify-content: space-between;
padding: 0.5em;
`
`;
export const Row2 = styled.div`
display: flex;
/* margin: 0.5em 0; */
justify-content: space-between;
padding: 0.5em;
`
`;
export const Column = styled.div`
display: flex;
flex-direction: column;
margin: 0em 1em;
justify-content: space-between;
`
`;
export const RowBorderGray = styled(Row)`
border: 1px solid gray;
/* border-radius: 0.5em; */
`
`;
export const RowLightBorderGray = styled(Row2)`
border: 1px solid lightgray;
@ -414,7 +410,7 @@ export const RowLightBorderGray = styled(Row2)`
border: 1px solid lightgray;
background-color: red;
}
`
`;
export const HistoryRow = styled.a`
text-decoration: none;
@ -423,7 +419,7 @@ export const HistoryRow = styled.a`
display: flex;
justify-content: space-between;
padding: 0.5em;
border: 1px solid lightgray;
border-top: 0px;
@ -439,7 +435,7 @@ export const HistoryRow = styled.a`
margin-left: auto;
align-self: center;
}
`
`;
export const ListOfProducts = styled.div`
& > div > a > img {
@ -453,62 +449,62 @@ export const ListOfProducts = styled.div`
margin-right: auto;
margin-left: 1em;
}
`
`;
export const LightText = styled.div`
color: gray;
`
`;
export const WarningText = styled.div`
color: rgb(223, 117, 20);
`
`;
export const SmallText = styled.div`
font-size: small;
`
font-size: small;
`;
export const LargeText = styled.div`
font-size: large;
`
font-size: large;
`;
export const ExtraLargeText = styled.div`
font-size: x-large;
`
font-size: x-large;
`;
export const SmallLightText = styled(SmallText)`
color: gray;
`
`;
export const CenteredText = styled.div`
white-space: nowrap;
text-align: center;
`
`;
export const CenteredBoldText = styled(CenteredText)`
white-space: nowrap;
text-align: center;
font-weight: bold;
color: ${((props: any): any => String(props.color) as any) as any};
`
`;
export const Input = styled.div<{ invalid?: boolean }>`
& label {
display: block;
padding: 5px;
color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
& input {
display: block;
padding: 5px;
width: calc(100% - 4px - 10px);
border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
`
`;
export const InputWithLabel = styled.div<{ invalid?: boolean }>`
& label {
display: block;
padding: 5px;
color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
& > div {
position: relative;
@ -516,20 +512,20 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
top: 0px;
bottom: 0px;
& > div {
& > div {
position: absolute;
background-color: lightgray;
padding: 5px;
margin: 2px;
}
& > input {
& > input {
flex: 1;
padding: 5px;
border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
padding: 5px;
border-color: ${({ invalid }) => (!invalid ? "inherit" : "red")};
}
}
`
`;
export const ErrorBox = styled.div`
border: 2px solid #f5c6cb;
@ -555,22 +551,22 @@ export const ErrorBox = styled.div`
width: 28px;
}
}
`
`;
export const SuccessBox = styled(ErrorBox)`
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
`
`;
export const WarningBox = styled(ErrorBox)`
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
`
`;
export const PopupNavigation = styled.div<{ devMode?: boolean }>`
background-color:#0042b2;
background-color: #0042b2;
height: 35px;
justify-content: space-around;
display: flex;
@ -582,7 +578,7 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
& > div > a {
color: #f8faf7;
display: inline-block;
width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5});
width: calc(400px / ${({ devMode }) => (!devMode ? 4 : 5)});
text-align: center;
text-decoration: none;
vertical-align: middle;
@ -597,7 +593,6 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
`;
export const NiceSelect = styled.div`
& > select {
-webkit-appearance: none;
-moz-appearance: none;
@ -617,19 +612,19 @@ export const NiceSelect = styled.div`
display: flex;
/* width: 10em; */
overflow: hidden;
border-radius: .25em;
border-radius: 0.25em;
&::after {
content: '\u25BC';
content: "\u25BC";
position: absolute;
top: 0;
right: 0;
padding: 0.5em 1em;
cursor: pointer;
pointer-events: none;
-webkit-transition: .25s all ease;
-o-transition: .25s all ease;
transition: .25s all ease;
-webkit-transition: 0.25s all ease;
-o-transition: 0.25s all ease;
transition: 0.25s all ease;
}
&:hover::after {
@ -639,7 +634,7 @@ export const NiceSelect = styled.div`
&::-ms-expand {
display: none;
}
`
`;
export const Outlined = styled.div`
border: 2px solid #388e3c;
@ -647,13 +642,12 @@ export const Outlined = styled.div`
width: fit-content;
border-radius: 2px;
color: #388e3c;
`
`;
/* { width: "1.5em", height: "1.5em", verticalAlign: "middle" } */
export const CheckboxSuccess = styled.input`
vertical-align: center;
`
`;
export const TermsSection = styled.a`
border: 1px solid black;
@ -664,13 +658,13 @@ export const TermsSection = styled.a`
text-decoration: none;
color: inherit;
flex-direction: column;
display: flex;
&[data-open="true"] {
display: flex;
display: flex;
}
&[data-open="false"] > *:not(:first-child) {
display: none;
display: none;
}
header {
@ -681,11 +675,11 @@ export const TermsSection = styled.a`
height: auto;
}
&[data-open="true"] header:after {
content: '\\2227';
&[data-open="true"] header:after {
content: "\\2227";
}
&[data-open="false"] header:after {
content: '\\2228';
&[data-open="false"] header:after {
content: "\\2228";
}
`;
@ -712,13 +706,13 @@ export const TermsOfService = styled.div`
padding: 1em;
margin-top: 2px;
margin-bottom: 2px;
display: flex;
&[data-open="true"] {
display: flex;
display: flex;
}
&[data-open="false"] > *:not(:first-child) {
display: none;
display: none;
}
header {
@ -729,22 +723,20 @@ export const TermsOfService = styled.div`
height: auto;
}
&[data-open="true"] > header:after {
content: '\\2227';
&[data-open="true"] > header:after {
content: "\\2227";
}
&[data-open="false"] > header:after {
content: '\\2228';
&[data-open="false"] > header:after {
content: "\\2228";
}
}
`
`;
export const StyledCheckboxLabel = styled.div`
color: green;
text-transform: uppercase;
/* font-weight: bold; */
text-align: center;
span {
input {
display: none;
opacity: 0;
@ -758,7 +750,7 @@ export const StyledCheckboxLabel = styled.div`
margin-right: 1em;
border-radius: 2px;
border: 2px solid currentColor;
svg {
transition: transform 0.1s ease-in 25ms;
transform: scale(0);
@ -776,12 +768,11 @@ export const StyledCheckboxLabel = styled.div`
}
input:disabled + div {
color: #959495;
};
}
input:disabled + div + label {
color: #959495;
};
}
input:focus + div + label {
box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;
}
`
`;

View File

@ -15,13 +15,13 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from 'preact'
import { useContext, useState } from 'preact/hooks'
import { useLocalStorage } from '../hooks/useLocalStorage';
import { createContext, h, VNode } from "preact";
import { useContext, useState } from "preact/hooks";
import { useLocalStorage } from "../hooks/useLocalStorage";
interface Type {
devMode: boolean;
@ -29,14 +29,14 @@ interface Type {
}
const Context = createContext<Type>({
devMode: false,
toggleDevMode: () => null
})
toggleDevMode: () => null,
});
export const useDevContext = (): Type => useContext(Context);
export const DevContextProvider = ({ children }: { children: any }): VNode => {
const [value, setter] = useLocalStorage('devMode')
const devMode = value === "true"
const toggleDevMode = () => setter(v => !v ? "true" : undefined)
const [value, setter] = useLocalStorage("devMode");
const devMode = value === "true";
const toggleDevMode = () => setter((v) => (!v ? "true" : undefined));
return h(Context.Provider, { value: { devMode, toggleDevMode }, children });
}
};

View File

@ -15,54 +15,58 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from 'preact'
import { useContext, useEffect } from 'preact/hooks'
import { useLang } from '../hooks/useLang'
import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks/useLang";
//@ts-ignore: type declaration
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
import { setupI18n } from '@gnu-taler/taler-util';
import { setupI18n } from "@gnu-taler/taler-util";
interface Type {
lang: string;
changeLanguage: (l: string) => void;
}
const initial = {
lang: 'en',
lang: "en",
changeLanguage: () => {
// do not change anything
}
}
const Context = createContext<Type>(initial)
},
};
const Context = createContext<Type>(initial);
interface Props {
initial?: string,
children: any,
forceLang?: string
initial?: string;
children: any;
forceLang?: string;
}
//we use forceLang when we don't want to use the saved state, but sone forced
//runtime lang predefined lang
export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
const [lang, changeLanguage] = useLang(initial)
//we use forceLang when we don't want to use the saved state, but sone forced
//runtime lang predefined lang
export const TranslationProvider = ({
initial,
children,
forceLang,
}: Props): VNode => {
const [lang, changeLanguage] = useLang(initial);
useEffect(() => {
if (forceLang) {
changeLanguage(forceLang)
changeLanguage(forceLang);
}
})
useEffect(()=> {
setupI18n(lang, strings)
},[lang])
});
useEffect(() => {
setupI18n(lang, strings);
}, [lang]);
if (forceLang) {
setupI18n(forceLang, strings)
setupI18n(forceLang, strings);
} else {
setupI18n(lang, strings)
setupI18n(lang, strings);
}
return h(Context.Provider, { value: { lang, changeLanguage }, children });
}
};
export const useTranslationContext = (): Type => useContext(Context);

View File

@ -15,150 +15,156 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util';
import { createExample } from '../test-utils';
import { PaymentRequestView as TestedComponent } from './Pay';
import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { PaymentRequestView as TestedComponent } from "./Pay";
export default {
title: 'cta/pay',
title: "cta/pay",
component: TestedComponent,
argTypes: {
},
argTypes: {},
};
export const NoBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
noncePriv: '',
noncePriv: "",
proposalId: "proposal1234",
contractTerms: {
contractTerms: ({
merchant: {
name: 'someone'
name: "someone",
},
summary: 'some beers',
amount: 'USD:10',
} as Partial<ContractTerms> as any,
amountRaw: 'USD:10',
}
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms>) as any,
amountRaw: "USD:10",
},
});
export const NoEnoughBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
noncePriv: '',
noncePriv: "",
proposalId: "proposal1234",
contractTerms: {
contractTerms: ({
merchant: {
name: 'someone'
name: "someone",
},
summary: 'some beers',
amount: 'USD:10',
} as Partial<ContractTerms> as any,
amountRaw: 'USD:10',
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms>) as any,
amountRaw: "USD:10",
},
balance: {
currency: 'USD',
currency: "USD",
fraction: 40000000,
value: 9
}
value: 9,
},
});
export const PaymentPossible = createExample(TestedComponent, {
uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
uri:
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: 'USD:10',
amountRaw: 'USD:10',
noncePriv: '',
contractTerms: {
nonce: '123213123',
amountEffective: "USD:10",
amountRaw: "USD:10",
noncePriv: "",
contractTerms: ({
nonce: "123213123",
merchant: {
name: 'someone'
name: "someone",
},
amount: 'USD:10',
summary: 'some beers',
} as Partial<ContractTerms> as any,
contractTermsHash: '123456',
proposalId: 'proposal1234'
}
amount: "USD:10",
summary: "some beers",
} as Partial<ContractTerms>) as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
},
});
export const PaymentPossibleWithFee = createExample(TestedComponent, {
uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
uri:
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: 'USD:10.20',
amountRaw: 'USD:10',
noncePriv: '',
contractTerms: {
nonce: '123213123',
amountEffective: "USD:10.20",
amountRaw: "USD:10",
noncePriv: "",
contractTerms: ({
nonce: "123213123",
merchant: {
name: 'someone'
name: "someone",
},
amount: 'USD:10',
summary: 'some beers',
} as Partial<ContractTerms> as any,
contractTermsHash: '123456',
proposalId: 'proposal1234'
}
amount: "USD:10",
summary: "some beers",
} as Partial<ContractTerms>) as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
},
});
export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: 'USD:10',
amountRaw: 'USD:10',
contractTerms: {
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: ({
merchant: {
name: 'someone'
name: "someone",
},
fulfillment_message: 'congratulations! you are looking at the fulfillment message! ',
summary: 'some beers',
amount: 'USD:10',
} as Partial<ContractTerms> as any,
contractTermsHash: '123456',
proposalId: 'proposal1234',
fulfillment_message:
"congratulations! you are looking at the fulfillment message! ",
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms>) as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: false,
}
},
});
export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: 'USD:10',
amountRaw: 'USD:10',
contractTerms: {
merchant: {
name: 'someone'
},
summary: 'some beers',
amount: 'USD:10',
} as Partial<ContractTerms> as any,
contractTermsHash: '123456',
proposalId: 'proposal1234',
paid: false,
}
});
export const AlreadyConfirmedWithoutFullfilment = createExample(
TestedComponent,
{
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: ({
merchant: {
name: "someone",
},
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms>) as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: false,
},
},
);
export const AlreadyPaid = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.AlreadyConfirmed,
amountEffective: 'USD:10',
amountRaw: 'USD:10',
contractTerms: {
amountEffective: "USD:10",
amountRaw: "USD:10",
contractTerms: ({
merchant: {
name: 'someone'
name: "someone",
},
fulfillment_message: 'congratulations! you are looking at the fulfillment message! ',
summary: 'some beers',
amount: 'USD:10',
} as Partial<ContractTerms> as any,
contractTermsHash: '123456',
proposalId: 'proposal1234',
fulfillment_message:
"congratulations! you are looking at the fulfillment message! ",
summary: "some beers",
amount: "USD:10",
} as Partial<ContractTerms>) as any,
contractTermsHash: "123456",
proposalId: "proposal1234",
paid: true,
}
},
});

View File

@ -24,18 +24,36 @@
*/
// import * as i18n from "../i18n";
import { AmountJson, AmountLike, Amounts, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, ContractTerms, getJsonI18n, i18n, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
import { Fragment, JSX, VNode } from "preact";
import {
AmountJson,
AmountLike,
Amounts,
ConfirmPayResult,
ConfirmPayResultDone,
ConfirmPayResultType,
ContractTerms,
i18n,
PreparePayResult,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { h, Fragment, JSX, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { LogoHeader } from "../components/LogoHeader";
import { Part } from "../components/Part";
import { QR } from "../components/QR";
import { ButtonSuccess, ErrorBox, LinkSuccess, SuccessBox, WalletAction, WarningBox } from "../components/styled";
import {
ButtonSuccess,
ErrorBox,
LinkSuccess,
SuccessBox,
WalletAction,
WarningBox,
} from "../components/styled";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
interface Props {
talerPayUri?: string
talerPayUri?: string;
}
// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) {
@ -64,7 +82,9 @@ interface Props {
// </section>
// }
const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultDone> => {
const doPayment = async (
payStatus: PreparePayResult,
): Promise<ConfirmPayResultDone> => {
if (payStatus.status !== "payment-possible") {
throw Error(`invalid state: ${payStatus.status}`);
}
@ -80,18 +100,29 @@ const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultD
return res;
};
export function PayPage({ talerPayUri }: Props): JSX.Element {
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined);
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(
undefined,
);
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
undefined,
);
const [payErrMsg, setPayErrMsg] = useState<string | undefined>(undefined);
const balance = useBalances()
const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
const balance = useBalances();
const balanceWithoutError = balance?.hasError
? []
: balance?.response.balances || [];
const foundBalance = balanceWithoutError.find(b => payStatus && Amounts.parseOrThrow(b.available).currency === Amounts.parseOrThrow(payStatus?.amountRaw).currency)
const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined
const foundBalance = balanceWithoutError.find(
(b) =>
payStatus &&
Amounts.parseOrThrow(b.available).currency ===
Amounts.parseOrThrow(payStatus?.amountRaw).currency,
);
const foundAmount = foundBalance
? Amounts.parseOrThrow(foundBalance.available)
: undefined;
useEffect(() => {
if (!talerPayUri) return;
@ -101,7 +132,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayStatus(p);
} catch (e) {
if (e instanceof Error) {
setPayErrMsg(e.message)
setPayErrMsg(e.message);
}
}
};
@ -109,30 +140,28 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
}, [talerPayUri]);
if (!talerPayUri) {
return <span>missing pay uri</span>
return <span>missing pay uri</span>;
}
if (!payStatus) {
if (payErrMsg) {
return <WalletAction>
<LogoHeader />
<h2>
{i18n.str`Digital cash payment`}
</h2>
<section>
<p>Could not get the payment information for this order</p>
<ErrorBox>
{payErrMsg}
</ErrorBox>
</section>
</WalletAction>
return (
<WalletAction>
<LogoHeader />
<h2>{i18n.str`Digital cash payment`}</h2>
<section>
<p>Could not get the payment information for this order</p>
<ErrorBox>{payErrMsg}</ErrorBox>
</section>
</WalletAction>
);
}
return <span>Loading payment information ...</span>;
}
const onClick = async () => {
try {
const res = await doPayment(payStatus)
const res = await doPayment(payStatus);
setPayResult(res);
} catch (e) {
console.error(e);
@ -140,13 +169,18 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayErrMsg(e.message);
}
}
};
}
return <PaymentRequestView uri={talerPayUri}
payStatus={payStatus} payResult={payResult}
onClick={onClick} payErrMsg={payErrMsg}
balance={foundAmount} />;
return (
<PaymentRequestView
uri={talerPayUri}
payStatus={payStatus}
payResult={payResult}
onClick={onClick}
payErrMsg={payErrMsg}
balance={foundAmount}
/>
);
}
export interface PaymentRequestViewProps {
@ -157,7 +191,14 @@ export interface PaymentRequestViewProps {
uri: string;
balance: AmountJson | undefined;
}
export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrMsg, balance }: PaymentRequestViewProps) {
export function PaymentRequestView({
uri,
payStatus,
payResult,
onClick,
payErrMsg,
balance,
}: PaymentRequestViewProps) {
let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
const contractTerms: ContractTerms = payStatus.contractTerms;
@ -185,116 +226,174 @@ export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrM
}
function Alternative() {
const [showQR, setShowQR] = useState<boolean>(false)
const privateUri = payStatus.status !== PreparePayResultType.AlreadyConfirmed ? `${uri}&n=${payStatus.noncePriv}` : uri
return <section>
<LinkSuccess upperCased onClick={() => setShowQR(qr => !qr)}>
{!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`}
</LinkSuccess>
{showQR && <div>
<QR text={privateUri} />
Scan the QR code or <a href={privateUri}>click here</a>
</div>}
</section>
const [showQR, setShowQR] = useState<boolean>(false);
const privateUri =
payStatus.status !== PreparePayResultType.AlreadyConfirmed
? `${uri}&n=${payStatus.noncePriv}`
: uri;
return (
<section>
<LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
{!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`}
</LinkSuccess>
{showQR && (
<div>
<QR text={privateUri} />
Scan the QR code or <a href={privateUri}>click here</a>
</div>
)}
</section>
);
}
function ButtonsSection() {
if (payResult) {
if (payResult.type === ConfirmPayResultType.Pending) {
return <section>
<div>
<p>Processing...</p>
</div>
</section>
return (
<section>
<div>
<p>Processing...</p>
</div>
</section>
);
}
return null
return null;
}
if (payErrMsg) {
return <section>
<div>
<p>Payment failed: {payErrMsg}</p>
<button class="pure-button button-success" onClick={onClick} >
{i18n.str`Retry`}
</button>
</div>
</section>
return (
<section>
<div>
<p>Payment failed: {payErrMsg}</p>
<button class="pure-button button-success" onClick={onClick}>
{i18n.str`Retry`}
</button>
</div>
</section>
);
}
if (payStatus.status === PreparePayResultType.PaymentPossible) {
return <Fragment>
<section>
<ButtonSuccess upperCased onClick={onClick}>
{i18n.str`Pay`} {amountToString(payStatus.amountEffective)}
</ButtonSuccess>
</section>
<Alternative />
</Fragment>
return (
<Fragment>
<section>
<ButtonSuccess upperCased onClick={onClick}>
{i18n.str`Pay`} {amountToString(payStatus.amountEffective)}
</ButtonSuccess>
</section>
<Alternative />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
return <Fragment>
<section>
{balance ? <WarningBox>
Your balance of {amountToString(balance)} is not enough to pay for this purchase
</WarningBox> : <WarningBox>
Your balance is not enough to pay for this purchase.
</WarningBox>}
</section>
<section>
<ButtonSuccess upperCased>
{i18n.str`Withdraw digital cash`}
</ButtonSuccess>
</section>
<Alternative />
</Fragment>
return (
<Fragment>
<section>
{balance ? (
<WarningBox>
Your balance of {amountToString(balance)} is not enough to pay
for this purchase
</WarningBox>
) : (
<WarningBox>
Your balance is not enough to pay for this purchase.
</WarningBox>
)}
</section>
<section>
<ButtonSuccess upperCased>
{i18n.str`Withdraw digital cash`}
</ButtonSuccess>
</section>
<Alternative />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
return <Fragment>
<section>
{payStatus.paid && contractTerms.fulfillment_message && <Part title="Merchant message" text={contractTerms.fulfillment_message} kind='neutral' />}
</section>
{!payStatus.paid && <Alternative />}
</Fragment>
return (
<Fragment>
<section>
{payStatus.paid && contractTerms.fulfillment_message && (
<Part
title="Merchant message"
text={contractTerms.fulfillment_message}
kind="neutral"
/>
)}
</section>
{!payStatus.paid && <Alternative />}
</Fragment>
);
}
return <span />
return <span />;
}
return <WalletAction>
<LogoHeader />
return (
<WalletAction>
<LogoHeader />
<h2>
{i18n.str`Digital cash payment`}
</h2>
{payStatus.status === PreparePayResultType.AlreadyConfirmed &&
(payStatus.paid ? <SuccessBox> Already paid </SuccessBox> : <WarningBox> Already claimed </WarningBox>)
}
{payResult && payResult.type === ConfirmPayResultType.Done && (
<SuccessBox>
<h3>Payment complete</h3>
<p>{!payResult.contractTerms.fulfillment_message ?
"You will now be sent back to the merchant you came from." :
payResult.contractTerms.fulfillment_message
}</p>
</SuccessBox>
)}
<section>
{payStatus.status !== PreparePayResultType.InsufficientBalance && Amounts.isNonZero(totalFees) &&
<Part big title="Total to pay" text={amountToString(payStatus.amountEffective)} kind='negative' />
}
<Part big title="Purchase amount" text={amountToString(payStatus.amountRaw)} kind='neutral' />
{Amounts.isNonZero(totalFees) && <Fragment>
<Part big title="Fee" text={amountToString(totalFees)} kind='negative' />
</Fragment>
}
<Part title="Merchant" text={contractTerms.merchant.name} kind='neutral' />
<Part title="Purchase" text={contractTerms.summary} kind='neutral' />
{contractTerms.order_id && <Part title="Receipt" text={`#${contractTerms.order_id}`} kind='neutral' />}
</section>
<ButtonsSection />
</WalletAction>
<h2>{i18n.str`Digital cash payment`}</h2>
{payStatus.status === PreparePayResultType.AlreadyConfirmed &&
(payStatus.paid ? (
<SuccessBox> Already paid </SuccessBox>
) : (
<WarningBox> Already claimed </WarningBox>
))}
{payResult && payResult.type === ConfirmPayResultType.Done && (
<SuccessBox>
<h3>Payment complete</h3>
<p>
{!payResult.contractTerms.fulfillment_message
? "You will now be sent back to the merchant you came from."
: payResult.contractTerms.fulfillment_message}
</p>
</SuccessBox>
)}
<section>
{payStatus.status !== PreparePayResultType.InsufficientBalance &&
Amounts.isNonZero(totalFees) && (
<Part
big
title="Total to pay"
text={amountToString(payStatus.amountEffective)}
kind="negative"
/>
)}
<Part
big
title="Purchase amount"
text={amountToString(payStatus.amountRaw)}
kind="neutral"
/>
{Amounts.isNonZero(totalFees) && (
<Fragment>
<Part
big
title="Fee"
text={amountToString(totalFees)}
kind="negative"
/>
</Fragment>
)}
<Part
title="Merchant"
text={contractTerms.merchant.name}
kind="neutral"
/>
<Part title="Purchase" text={contractTerms.summary} kind="neutral" />
{contractTerms.order_id && (
<Part
title="Receipt"
text={`#${contractTerms.order_id}`}
kind="neutral"
/>
)}
</section>
<ButtonsSection />
</WalletAction>
);
}
function amountToString(text: AmountLike) {
const aj = Amounts.jsonifyAmount(text)
const amount = Amounts.stringifyValue(aj, 2)
return `${amount} ${aj.currency}`
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj, 2);
return `${amount} ${aj.currency}`;
}

View File

@ -15,63 +15,61 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { OrderShortInfo } from '@gnu-taler/taler-util';
import { createExample } from '../test-utils';
import { View as TestedComponent } from './Refund';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { OrderShortInfo } from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { View as TestedComponent } from "./Refund";
export default {
title: 'cta/refund',
title: "cta/refund",
component: TestedComponent,
argTypes: {
},
argTypes: {},
};
export const Complete = createExample(TestedComponent, {
applyResult: {
amountEffectivePaid: 'USD:10',
amountRefundGone: 'USD:0',
amountRefundGranted: 'USD:2',
contractTermsHash: 'QWEASDZXC',
info: {
summary: 'tasty cold beer',
contractTermsHash: 'QWEASDZXC',
} as Partial<OrderShortInfo> as any,
amountEffectivePaid: "USD:10",
amountRefundGone: "USD:0",
amountRefundGranted: "USD:2",
contractTermsHash: "QWEASDZXC",
info: ({
summary: "tasty cold beer",
contractTermsHash: "QWEASDZXC",
} as Partial<OrderShortInfo>) as any,
pendingAtExchange: false,
proposalId: "proposal123",
}
},
});
export const Partial = createExample(TestedComponent, {
applyResult: {
amountEffectivePaid: 'USD:10',
amountRefundGone: 'USD:1',
amountRefundGranted: 'USD:2',
contractTermsHash: 'QWEASDZXC',
info: {
summary: 'tasty cold beer',
contractTermsHash: 'QWEASDZXC',
} as Partial<OrderShortInfo> as any,
amountEffectivePaid: "USD:10",
amountRefundGone: "USD:1",
amountRefundGranted: "USD:2",
contractTermsHash: "QWEASDZXC",
info: ({
summary: "tasty cold beer",
contractTermsHash: "QWEASDZXC",
} as Partial<OrderShortInfo>) as any,
pendingAtExchange: false,
proposalId: "proposal123",
}
},
});
export const InProgress = createExample(TestedComponent, {
applyResult: {
amountEffectivePaid: 'USD:10',
amountRefundGone: 'USD:1',
amountRefundGranted: 'USD:2',
contractTermsHash: 'QWEASDZXC',
info: {
summary: 'tasty cold beer',
contractTermsHash: 'QWEASDZXC',
} as Partial<OrderShortInfo> as any,
amountEffectivePaid: "USD:10",
amountRefundGone: "USD:1",
amountRefundGranted: "USD:2",
contractTermsHash: "QWEASDZXC",
info: ({
summary: "tasty cold beer",
contractTermsHash: "QWEASDZXC",
} as Partial<OrderShortInfo>) as any,
pendingAtExchange: true,
proposalId: "proposal123",
}
},
});

View File

@ -22,45 +22,46 @@
import * as wxApi from "../wxApi";
import { AmountView } from "../renderHtml";
import {
ApplyRefundResponse,
Amounts,
} from "@gnu-taler/taler-util";
import { ApplyRefundResponse, Amounts } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
import { h } from 'preact';
import { h } from "preact";
interface Props {
talerRefundUri?: string
talerRefundUri?: string;
}
export interface ViewProps {
applyResult: ApplyRefundResponse;
}
export function View({ applyResult }: ViewProps) {
return <section class="main">
<h1>GNU Taler Wallet</h1>
<article class="fade">
<h2>Refund Status</h2>
<p>
The product <em>{applyResult.info.summary}</em> has received a total
effective refund of{" "}
<AmountView amount={applyResult.amountRefundGranted} />.
</p>
{applyResult.pendingAtExchange ? (
<p>Refund processing is still in progress.</p>
) : null}
{!Amounts.isZero(applyResult.amountRefundGone) ? (
return (
<section class="main">
<h1>GNU Taler Wallet</h1>
<article class="fade">
<h2>Refund Status</h2>
<p>
The refund amount of{" "}
<AmountView amount={applyResult.amountRefundGone} />{" "}
could not be applied.
The product <em>{applyResult.info.summary}</em> has received a total
effective refund of{" "}
<AmountView amount={applyResult.amountRefundGranted} />.
</p>
) : null}
</article>
</section>
{applyResult.pendingAtExchange ? (
<p>Refund processing is still in progress.</p>
) : null}
{!Amounts.isZero(applyResult.amountRefundGone) ? (
<p>
The refund amount of{" "}
<AmountView amount={applyResult.amountRefundGone} /> could not be
applied.
</p>
) : null}
</article>
</section>
);
}
export function RefundPage({ talerRefundUri }: Props): JSX.Element {
const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined);
const [applyResult, setApplyResult] = useState<
ApplyRefundResponse | undefined
>(undefined);
const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
useEffect(() => {

View File

@ -15,45 +15,43 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { View as TestedComponent } from './Tip';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../test-utils";
import { View as TestedComponent } from "./Tip";
export default {
title: 'cta/tip',
title: "cta/tip",
component: TestedComponent,
argTypes: {
},
argTypes: {},
};
export const Accepted = createExample(TestedComponent, {
prepareTipResult: {
accepted: true,
merchantBaseUrl: '',
exchangeBaseUrl: '',
expirationTimestamp : {
t_ms: 0
merchantBaseUrl: "",
exchangeBaseUrl: "",
expirationTimestamp: {
t_ms: 0,
},
tipAmountEffective: 'USD:10',
tipAmountRaw: 'USD:5',
walletTipId: 'id'
}
tipAmountEffective: "USD:10",
tipAmountRaw: "USD:5",
walletTipId: "id",
},
});
export const NotYetAccepted = createExample(TestedComponent, {
prepareTipResult: {
accepted: false,
merchantBaseUrl: 'http://merchant.url/',
exchangeBaseUrl: 'http://exchange.url/',
expirationTimestamp : {
t_ms: 0
merchantBaseUrl: "http://merchant.url/",
exchangeBaseUrl: "http://exchange.url/",
expirationTimestamp: {
t_ms: 0,
},
tipAmountEffective: 'USD:10',
tipAmountRaw: 'USD:5',
walletTipId: 'id'
}
tipAmountEffective: "USD:10",
tipAmountRaw: "USD:5",
walletTipId: "id",
},
});

View File

@ -25,43 +25,43 @@ import { PrepareTipResult } from "@gnu-taler/taler-util";
import { AmountView } from "../renderHtml";
import * as wxApi from "../wxApi";
import { JSX } from "preact/jsx-runtime";
import { h } from 'preact';
import { h } from "preact";
interface Props {
talerTipUri?: string
talerTipUri?: string;
}
export interface ViewProps {
prepareTipResult: PrepareTipResult;
onAccept: () => void;
onIgnore: () => void;
}
export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) {
return <section class="main">
<h1>GNU Taler Wallet</h1>
<article class="fade">
{prepareTipResult.accepted ? (
<span>
Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. Check
your transactions list for more details.
</span>
) : (
return (
<section class="main">
<h1>GNU Taler Wallet</h1>
<article class="fade">
{prepareTipResult.accepted ? (
<span>
Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted.
Check your transactions list for more details.
</span>
) : (
<div>
<p>
The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is
offering you a tip of{" "}
offering you a tip of{" "}
<strong>
<AmountView amount={prepareTipResult.tipAmountEffective} />
</strong>{" "}
via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
</p>
<button onClick={onAccept}>Accept tip</button>
<button onClick={onIgnore}>Ignore</button>
</div>
)}
</article>
</section>
</article>
</section>
);
}
export function TipPage({ talerTipUri }: Props): JSX.Element {
@ -105,7 +105,11 @@ export function TipPage({ talerTipUri }: Props): JSX.Element {
return <span>Loading ...</span>;
}
return <View prepareTipResult={prepareTipResult}
onAccept={doAccept} onIgnore={doIgnore}
/>
return (
<View
prepareTipResult={prepareTipResult}
onAccept={doAccept}
onIgnore={doIgnore}
/>
);
}

View File

@ -15,23 +15,22 @@
*/
/**
*
* @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 { createExample } from '../test-utils';
import { View as TestedComponent } from './Withdraw';
*
* @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 { createExample } from "../test-utils";
import { View as TestedComponent } from "./Withdraw";
export default {
title: 'cta/withdraw',
title: "cta/withdraw",
component: TestedComponent,
argTypes: {
onSwitchExchange: { action: 'onRetry' },
onSwitchExchange: { action: "onRetry" },
},
};
@ -48,7 +47,7 @@ const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
</div>
</body>
</html>
`
`;
const termsPlain = `
Terms Of Service
****************
@ -432,7 +431,7 @@ Questions or comments
We welcome comments, questions, concerns, or suggestions. Please send
us a message on our contact page at legal@taler-systems.com.
`
`;
const termsXml = `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd">
@ -781,120 +780,131 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>
`;
export const NewTerms = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: '',
contentType: '',
currentEtag: '',
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
currency: "USD",
fraction: 0,
value: 0
value: 0,
},
amount: {
currency: 'USD',
currency: "USD",
value: 2,
fraction: 10000000
fraction: 10000000,
},
onSwitchExchange: async () => { },
onSwitchExchange: async () => {},
terms: {
value: {
type: 'xml',
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: 'new'
status: "new",
},
})
});
export const TermsReviewingPLAIN = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: '',
contentType: '',
currentEtag: '',
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
currency: "USD",
fraction: 0,
value: 0
value: 0,
},
amount: {
currency: 'USD',
currency: "USD",
value: 2,
fraction: 10000000
fraction: 10000000,
},
onSwitchExchange: async () => { },
onSwitchExchange: async () => {},
terms: {
value: {
type: 'plain',
content: termsPlain
type: "plain",
content: termsPlain,
},
status: 'new'
status: "new",
},
reviewing: true
})
reviewing: true,
});
export const TermsReviewingHTML = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: '',
contentType: '',
currentEtag: '',
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
currency: "USD",
fraction: 0,
value: 0
value: 0,
},
amount: {
currency: 'USD',
currency: "USD",
value: 2,
fraction: 10000000
fraction: 10000000,
},
onSwitchExchange: async () => { },
onSwitchExchange: async () => {},
terms: {
value: {
type: 'html',
href: new URL(`data:text/html;base64,${Buffer.from(termsHtml).toString('base64')}`),
type: "html",
href: new URL(
`data:text/html;base64,${Buffer.from(termsHtml).toString("base64")}`,
),
},
status: 'new'
status: "new",
},
reviewing: true
})
reviewing: true,
});
const termsPdf = `
%PDF-1.2
@ -909,306 +919,330 @@ endobj
trailer
<< /Root 3 0 R >>
%%EOF
`
`;
export const TermsReviewingPDF = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
value: {
type: 'pdf',
location: new URL(`data:text/html;base64,${Buffer.from(termsPdf).toString('base64')}`),
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
status: 'new'
},
reviewing: true
})
export const TermsReviewingXML = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
value: {
type: 'xml',
document: new DOMParser().parseFromString(termsXml, "text/xml"),
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
status: 'new'
},
reviewing: true
})
export const NewTermsAccepted = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: '',
contentType: '',
currentEtag: '',
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
value: {
type: 'xml',
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: 'new'
},
reviewed: true
})
export const TermsShowAgainXML = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
value: {
type: 'xml',
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: 'new'
},
reviewed: true,
reviewing: true,
})
export const TermsChanged = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
value: {
type: 'xml',
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: 'changed'
},
})
export const TermsNotFound = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: 0,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
status: 'notfound'
},
})
export const TermsAlreadyAccepted = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
fraction: amountFractionalBase * 0.5,
value: 0
},
amount: {
currency: 'USD',
value: 2,
fraction: 10000000
},
onSwitchExchange: async () => { },
terms: {
status: 'accepted'
},
})
export const WithoutFee = createExample(TestedComponent, {
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'exchange.demo.taler.net',
paytoUris: ['asd'],
}, {
currency: 'USD',
exchangeBaseUrl: 'exchange.test.taler.net',
paytoUris: ['asd'],
}],
exchangeBaseUrl: 'exchange.demo.taler.net',
details: {
content: '',
contentType: '',
currentEtag: '',
acceptedEtag: undefined,
},
withdrawalFee: {
currency: 'USD',
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: 'USD',
currency: "USD",
value: 2,
fraction: 10000000
fraction: 10000000,
},
onSwitchExchange: async () => { },
onSwitchExchange: async () => {},
terms: {
value: {
type: 'xml',
type: "pdf",
location: new URL(
`data:text/html;base64,${Buffer.from(termsPdf).toString("base64")}`,
),
},
status: "new",
},
reviewing: true,
});
export const TermsReviewingXML = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
value: {
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: 'accepted',
}
})
status: "new",
},
reviewing: true,
});
export const NewTermsAccepted = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
value: {
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: "new",
},
reviewed: true,
});
export const TermsShowAgainXML = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
value: {
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: "new",
},
reviewed: true,
reviewing: true,
});
export const TermsChanged = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
value: {
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: "changed",
},
});
export const TermsNotFound = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
status: "notfound",
},
});
export const TermsAlreadyAccepted = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: amountFractionalBase * 0.5,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
status: "accepted",
},
});
export const WithoutFee = createExample(TestedComponent, {
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "exchange.demo.taler.net",
paytoUris: ["asd"],
},
{
currency: "USD",
exchangeBaseUrl: "exchange.test.taler.net",
paytoUris: ["asd"],
},
],
exchangeBaseUrl: "exchange.demo.taler.net",
details: {
content: "",
contentType: "",
currentEtag: "",
acceptedEtag: undefined,
},
withdrawalFee: {
currency: "USD",
fraction: 0,
value: 0,
},
amount: {
currency: "USD",
value: 2,
fraction: 10000000,
},
onSwitchExchange: async () => {},
terms: {
value: {
type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
status: "accepted",
},
});

View File

@ -21,21 +21,39 @@
* @author Florian Dold
*/
import { AmountJson, Amounts, ExchangeListItem, GetExchangeTosResult, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util';
import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
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';
import { Part } from '../components/Part';
import { SelectList } from '../components/SelectList';
import { ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, TermsOfService, WalletAction, WarningText } from '../components/styled';
import { useAsyncAsHook } from '../hooks/useAsyncAsHook';
import {
acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges, getExchangeTos
AmountJson,
Amounts,
ExchangeListItem,
GetExchangeTosResult,
i18n,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import { VNode, h } 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";
import { Part } from "../components/Part";
import { SelectList } from "../components/SelectList";
import {
ButtonSuccess,
ButtonWarning,
LinkSuccess,
TermsOfService,
WalletAction,
WarningText,
} from "../components/styled";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import {
acceptWithdrawal,
getExchangeTos,
getExchangeWithdrawalInfo,
getWithdrawalDetailsForUri,
listExchanges,
setExchangeTosAccepted,
} from "../wxApi";
import { wxMain } from '../wxBackend.js';
interface Props {
talerWithdrawUri?: string;
@ -58,145 +76,193 @@ export interface ViewProps {
status: TermsStatus;
};
knownExchanges: ExchangeListItem[];
}
};
type TermsStatus = "new" | "accepted" | "changed" | "notfound";
type TermsStatus = 'new' | 'accepted' | 'changed' | 'notfound';
type TermsDocument = TermsDocumentXml | TermsDocumentHtml | TermsDocumentPlain | TermsDocumentJson | TermsDocumentPdf;
type TermsDocument =
| TermsDocumentXml
| TermsDocumentHtml
| TermsDocumentPlain
| TermsDocumentJson
| TermsDocumentPdf;
interface TermsDocumentXml {
type: 'xml',
document: Document,
type: "xml";
document: Document;
}
interface TermsDocumentHtml {
type: 'html',
href: URL,
type: "html";
href: URL;
}
interface TermsDocumentPlain {
type: 'plain',
content: string,
type: "plain";
content: string;
}
interface TermsDocumentJson {
type: 'json',
data: any,
type: "json";
data: any;
}
interface TermsDocumentPdf {
type: 'pdf',
location: URL,
type: "pdf";
location: URL;
}
function amountToString(text: AmountJson) {
const aj = Amounts.jsonifyAmount(text)
const amount = Amounts.stringifyValue(aj)
return `${amount} ${aj.currency}`
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj);
return `${amount} ${aj.currency}`;
}
export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, amount, onWithdraw, onSwitchExchange, terms, reviewing, onReview, onAccept, reviewed, confirmed }: ViewProps) {
const needsReview = terms.status === 'changed' || terms.status === 'new'
export function View({
details,
withdrawalFee,
exchangeBaseUrl,
knownExchanges,
amount,
onWithdraw,
onSwitchExchange,
terms,
reviewing,
onReview,
onAccept,
reviewed,
confirmed,
}: ViewProps) {
const needsReview = terms.status === "changed" || terms.status === "new";
const [switchingExchange, setSwitchingExchange] = useState<string | undefined>(undefined)
const exchanges = knownExchanges.reduce((prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {})
const [switchingExchange, setSwitchingExchange] = useState<
string | undefined
>(undefined);
const exchanges = knownExchanges.reduce(
(prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }),
{},
);
return (
<WalletAction>
<LogoHeader />
<h2>
{i18n.str`Digital cash withdrawal`}
</h2>
<h2>{i18n.str`Digital cash withdrawal`}</h2>
<section>
<Part title="Total to withdraw" text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} kind='positive' />
<Part title="Chosen amount" text={amountToString(amount)} kind='neutral' />
{Amounts.isNonZero(withdrawalFee) &&
<Part title="Exchange fee" text={amountToString(withdrawalFee)} kind='negative' />
}
<Part title="Exchange" text={exchangeBaseUrl} kind='neutral' big />
<Part
title="Total to withdraw"
text={amountToString(Amounts.sub(amount, withdrawalFee).amount)}
kind="positive"
/>
<Part
title="Chosen amount"
text={amountToString(amount)}
kind="neutral"
/>
{Amounts.isNonZero(withdrawalFee) && (
<Part
title="Exchange fee"
text={amountToString(withdrawalFee)}
kind="negative"
/>
)}
<Part title="Exchange" text={exchangeBaseUrl} kind="neutral" big />
</section>
{!reviewing &&
{!reviewing && (
<section>
{switchingExchange !== undefined ? <Fragment>
<div>
<SelectList label="Known exchanges" list={exchanges} name="" onChange={onSwitchExchange} />
</div>
<LinkSuccess upperCased onClick={() => onSwitchExchange(switchingExchange)}>
{i18n.str`Confirm exchange selection`}
</LinkSuccess>
</Fragment>
: <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}>
{switchingExchange !== undefined ? (
<Fragment>
<div>
<SelectList
label="Known exchanges"
list={exchanges}
name=""
onChange={onSwitchExchange}
/>
</div>
<LinkSuccess
upperCased
onClick={() => onSwitchExchange(switchingExchange)}
>
{i18n.str`Confirm exchange selection`}
</LinkSuccess>
</Fragment>
) : (
<LinkSuccess upperCased onClick={() => setSwitchingExchange("")}>
{i18n.str`Switch exchange`}
</LinkSuccess>}
</LinkSuccess>
)}
</section>
}
{!reviewing && reviewed &&
)}
{!reviewing && reviewed && (
<section>
<LinkSuccess
upperCased
onClick={() => onReview(true)}
>
<LinkSuccess upperCased onClick={() => onReview(true)}>
{i18n.str`Show terms of service`}
</LinkSuccess>
</section>
}
{terms.status === 'notfound' &&
)}
{terms.status === "notfound" && (
<section>
<WarningText>
{i18n.str`Exchange doesn't have terms of service`}
</WarningText>
</section>
}
{reviewing &&
)}
{reviewing && (
<section>
{terms.status !== 'accepted' && terms.value && terms.value.type === 'xml' &&
<TermsOfService>
<ExchangeXmlTos doc={terms.value.document} />
</TermsOfService>
}
{terms.status !== 'accepted' && terms.value && terms.value.type === 'plain' &&
<div style={{ textAlign: 'left' }}>
<pre>{terms.value.content}</pre>
</div>
}
{terms.status !== 'accepted' && terms.value && terms.value.type === 'html' &&
<iframe src={terms.value.href.toString()} />
}
{terms.status !== 'accepted' && terms.value && terms.value.type === 'pdf' &&
<a href={terms.value.location.toString()} download="tos.pdf" >Download Terms of Service</a>
}
</section>}
{reviewing && reviewed &&
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "xml" && (
<TermsOfService>
<ExchangeXmlTos doc={terms.value.document} />
</TermsOfService>
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "plain" && (
<div style={{ textAlign: "left" }}>
<pre>{terms.value.content}</pre>
</div>
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "html" && (
<iframe src={terms.value.href.toString()} />
)}
{terms.status !== "accepted" &&
terms.value &&
terms.value.type === "pdf" && (
<a href={terms.value.location.toString()} download="tos.pdf">
Download Terms of Service
</a>
)}
</section>
)}
{reviewing && reviewed && (
<section>
<LinkSuccess
upperCased
onClick={() => onReview(false)}
>
<LinkSuccess upperCased onClick={() => onReview(false)}>
{i18n.str`Hide terms of service`}
</LinkSuccess>
</section>
}
{(reviewing || reviewed) &&
)}
{(reviewing || reviewed) && (
<section>
<CheckboxOutlined
name="terms"
enabled={reviewed}
label={i18n.str`I accept the exchange terms of service`}
onToggle={() => {
onAccept(!reviewed)
onReview(false)
onAccept(!reviewed);
onReview(false);
}}
/>
</section>
}
)}
{/**
* Main action section
*/}
<section>
{terms.status === 'new' && !reviewed && !reviewing &&
{terms.status === "new" && !reviewed && !reviewing && (
<ButtonSuccess
upperCased
disabled={!exchangeBaseUrl}
@ -204,8 +270,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Review exchange terms of service`}
</ButtonSuccess>
}
{terms.status === 'changed' && !reviewed && !reviewing &&
)}
{terms.status === "changed" && !reviewed && !reviewing && (
<ButtonWarning
upperCased
disabled={!exchangeBaseUrl}
@ -213,8 +279,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Review new version of terms of service`}
</ButtonWarning>
}
{(terms.status === 'accepted' || (needsReview && reviewed)) &&
)}
{(terms.status === "accepted" || (needsReview && reviewed)) && (
<ButtonSuccess
upperCased
disabled={!exchangeBaseUrl || confirmed}
@ -222,8 +288,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Confirm withdrawal`}
</ButtonSuccess>
}
{terms.status === 'notfound' &&
)}
{terms.status === "notfound" && (
<ButtonWarning
upperCased
disabled={!exchangeBaseUrl}
@ -231,60 +297,88 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Withdraw anyway`}
</ButtonWarning>
}
)}
</section>
</WalletAction>
)
);
}
export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) {
const [customExchange, setCustomExchange] = useState<string | undefined>(undefined)
const [errorAccepting, setErrorAccepting] = useState<string | undefined>(undefined)
export function WithdrawPageWithParsedURI({
uri,
uriInfo,
}: {
uri: string;
uriInfo: WithdrawUriInfoResponse;
}) {
const [customExchange, setCustomExchange] = useState<string | undefined>(
undefined,
);
const [errorAccepting, setErrorAccepting] = useState<string | undefined>(
undefined,
);
const [reviewing, setReviewing] = useState<boolean>(false)
const [reviewed, setReviewed] = useState<boolean>(false)
const [confirmed, setConfirmed] = useState<boolean>(false)
const [reviewing, setReviewing] = useState<boolean>(false);
const [reviewed, setReviewed] = useState<boolean>(false);
const [confirmed, setConfirmed] = useState<boolean>(false);
const knownExchangesHook = useAsyncAsHook(() => listExchanges())
const knownExchangesHook = useAsyncAsHook(() => listExchanges());
const knownExchanges = !knownExchangesHook || knownExchangesHook.hasError ? [] : knownExchangesHook.response.exchanges
const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount)
const thisCurrencyExchanges = knownExchanges.filter(ex => ex.currency === withdrawAmount.currency)
const knownExchanges =
!knownExchangesHook || knownExchangesHook.hasError
? []
: knownExchangesHook.response.exchanges;
const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount);
const thisCurrencyExchanges = knownExchanges.filter(
(ex) => ex.currency === withdrawAmount.currency,
);
const exchange = customExchange || uriInfo.defaultExchangeBaseUrl || thisCurrencyExchanges[0]?.exchangeBaseUrl
const exchange =
customExchange ||
uriInfo.defaultExchangeBaseUrl ||
thisCurrencyExchanges[0]?.exchangeBaseUrl;
const detailsHook = useAsyncAsHook(async () => {
if (!exchange) throw Error('no default exchange')
const tos = await getExchangeTos(exchange, ['text/xml'])
if (!exchange) throw Error("no default exchange");
const tos = await getExchangeTos(exchange, ["text/xml"]);
const info = await getExchangeWithdrawalInfo({
exchangeBaseUrl: exchange,
amount: withdrawAmount,
tosAcceptedFormat: ['text/xml']
})
return { tos, info }
})
tosAcceptedFormat: ["text/xml"],
});
return { tos, info };
});
if (!detailsHook) {
return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>;
return (
<span>
<i18n.Translate>Getting withdrawal details.</i18n.Translate>
</span>
);
}
if (detailsHook.hasError) {
return <span><i18n.Translate>Problems getting details: {detailsHook.message}</i18n.Translate></span>;
return (
<span>
<i18n.Translate>
Problems getting details: {detailsHook.message}
</i18n.Translate>
</span>
);
}
const details = detailsHook.response
const details = detailsHook.response;
const onAccept = async (): Promise<void> => {
try {
await setExchangeTosAccepted(exchange, details.tos.currentEtag)
setReviewed(true)
await setExchangeTosAccepted(exchange, details.tos.currentEtag);
setReviewed(true);
} catch (e) {
if (e instanceof Error) {
setErrorAccepting(e.message)
setErrorAccepting(e.message);
}
}
}
};
const onWithdraw = async (): Promise<void> => {
setConfirmed(true)
setConfirmed(true);
console.log("accepting exchange", exchange);
try {
const res = await acceptWithdrawal(uri, exchange);
@ -293,91 +387,121 @@ export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriIn
document.location.href = res.confirmTransferUrl;
}
} catch (e) {
setConfirmed(false)
setConfirmed(false);
}
};
const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(details.tos.contentType, details.tos.content);
const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(
details.tos.contentType,
details.tos.content,
);
const status: TermsStatus = !termsContent ? 'notfound' : (
!details.tos.acceptedEtag ? 'new' : (
details.tos.acceptedEtag !== details.tos.currentEtag ? 'changed' : 'accepted'
))
const status: TermsStatus = !termsContent
? "notfound"
: !details.tos.acceptedEtag
? "new"
: details.tos.acceptedEtag !== details.tos.currentEtag
? "changed"
: "accepted";
return <View onWithdraw={onWithdraw}
details={details.tos} amount={withdrawAmount}
exchangeBaseUrl={exchange}
withdrawalFee={details.info.withdrawFee} //FIXME
terms={{
status, value: termsContent
}}
onSwitchExchange={setCustomExchange}
knownExchanges={knownExchanges}
confirmed={confirmed}
reviewed={reviewed} onAccept={onAccept}
reviewing={reviewing} onReview={setReviewing}
/>
return (
<View
onWithdraw={onWithdraw}
details={details.tos}
amount={withdrawAmount}
exchangeBaseUrl={exchange}
withdrawalFee={details.info.withdrawFee} //FIXME
terms={{
status,
value: termsContent,
}}
onSwitchExchange={setCustomExchange}
knownExchanges={knownExchanges}
confirmed={confirmed}
reviewed={reviewed}
onAccept={onAccept}
reviewing={reviewing}
onReview={setReviewing}
/>
);
}
export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element {
const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) :
getWithdrawalDetailsForUri({ talerWithdrawUri })
)
export function WithdrawPage({ talerWithdrawUri }: Props): VNode {
const uriInfoHook = useAsyncAsHook(() =>
!talerWithdrawUri
? Promise.reject(undefined)
: getWithdrawalDetailsForUri({ talerWithdrawUri }),
);
if (!talerWithdrawUri) {
return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>;
return (
<span>
<i18n.Translate>missing withdraw uri</i18n.Translate>
</span>
);
}
if (!uriInfoHook) {
return <span><i18n.Translate>Loading...</i18n.Translate></span>;
return (
<span>
<i18n.Translate>Loading...</i18n.Translate>
</span>
);
}
if (uriInfoHook.hasError) {
return <span><i18n.Translate>This URI is not valid anymore: {uriInfoHook.message}</i18n.Translate></span>;
return (
<span>
<i18n.Translate>
This URI is not valid anymore: {uriInfoHook.message}
</i18n.Translate>
</span>
);
}
return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} />
return (
<WithdrawPageWithParsedURI
uri={talerWithdrawUri}
uriInfo={uriInfoHook.response}
/>
);
}
function parseTermsOfServiceContent(type: string, text: string): TermsDocument | undefined {
if (type === 'text/xml') {
function parseTermsOfServiceContent(
type: string,
text: string,
): TermsDocument | undefined {
if (type === "text/xml") {
try {
const document = new DOMParser().parseFromString(text, "text/xml")
return { type: 'xml', document }
const document = new DOMParser().parseFromString(text, "text/xml");
return { type: "xml", document };
} catch (e) {
console.log(e)
debugger;
console.log(e);
}
} else if (type === 'text/html') {
} else if (type === "text/html") {
try {
const href = new URL(text)
return { type: 'html', href }
const href = new URL(text);
return { type: "html", href };
} catch (e) {
console.log(e)
debugger;
console.log(e);
}
} else if (type === 'text/json') {
} else if (type === "text/json") {
try {
const data = JSON.parse(text)
return { type: 'json', data }
const data = JSON.parse(text);
return { type: "json", data };
} catch (e) {
console.log(e)
debugger;
console.log(e);
}
} else if (type === 'text/pdf') {
} else if (type === "text/pdf") {
try {
const location = new URL(text)
return { type: 'pdf', location }
const location = new URL(text);
return { type: "pdf", location };
} catch (e) {
console.log(e)
debugger;
console.log(e);
}
} else if (type === 'text/plain') {
} else if (type === "text/plain") {
try {
const content = text
return { type: 'plain', content }
const content = text;
return { type: "plain", content };
} catch (e) {
console.log(e)
debugger;
console.log(e);
}
}
return undefined
return undefined;
}

View File

@ -15,7 +15,7 @@
*/
import { JSX } from "preact/jsx-runtime";
import { h } from 'preact';
import { h } from "preact";
/**
* View and edit auditors.

View File

@ -63,7 +63,7 @@ class ResetNotification extends Component<any, State> {
type="checkbox"
checked={this.state.checked}
onChange={() => {
this.setState(prev => ({ checked: prev.checked }))
this.setState((prev) => ({ checked: prev.checked }));
}}
/>{" "}
<label htmlFor="check">

View File

@ -15,7 +15,7 @@
*/
import { JSX } from "preact/jsx-runtime";
import { h } from 'preact';
import { h } from "preact";
/**
* Return coins to own bank account.
*

View File

@ -21,7 +21,7 @@ declare module "*.png" {
const content: any;
export default content;
}
declare module '*.svg' {
declare module "*.svg" {
const content: any;
export default content;
}

View File

@ -29,7 +29,7 @@ interface HookError {
export type HookResponse<T> = HookOk<T> | HookError | undefined;
export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
useEffect(() => {
async function doAsync() {
@ -42,7 +42,7 @@ export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
}
}
}
doAsync()
doAsync();
}, []);
return result;
}

View File

@ -17,34 +17,31 @@
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
export interface BackupDeviceName {
name: string;
update: (s:string) => Promise<void>
update: (s: string) => Promise<void>;
}
export function useBackupDeviceName(): BackupDeviceName {
const [status, setStatus] = useState<BackupDeviceName>({
name: '',
update: () => Promise.resolve()
})
name: "",
update: () => Promise.resolve(),
});
useEffect(() => {
async function run() {
//create a first list of backup info by currency
const status = await wxApi.getBackupInfo()
const status = await wxApi.getBackupInfo();
async function update(newName: string) {
await wxApi.setWalletDeviceId(newName)
setStatus(old => ({ ...old, name: newName }))
await wxApi.setWalletDeviceId(newName);
setStatus((old) => ({ ...old, name: newName }));
}
setStatus({ name: status.deviceId, update })
setStatus({ name: status.deviceId, update });
}
run()
}, [])
run();
}, []);
return status
return status;
}

View File

@ -14,11 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ProviderInfo, ProviderPaymentPaid, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import {
ProviderInfo,
ProviderPaymentPaid,
ProviderPaymentStatus,
ProviderPaymentType,
} from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
export interface BackupStatus {
deviceName: string;
providers: ProviderInfo[];
@ -32,40 +36,46 @@ function getStatusTypeOrder(t: ProviderPaymentStatus) {
ProviderPaymentType.Unpaid,
ProviderPaymentType.Paid,
ProviderPaymentType.Pending,
].indexOf(t.type)
].indexOf(t.type);
}
function getStatusPaidOrder(a: ProviderPaymentPaid, b: ProviderPaymentPaid) {
return a.paidUntil.t_ms === 'never' ? -1 :
b.paidUntil.t_ms === 'never' ? 1 :
a.paidUntil.t_ms - b.paidUntil.t_ms
return a.paidUntil.t_ms === "never"
? -1
: b.paidUntil.t_ms === "never"
? 1
: a.paidUntil.t_ms - b.paidUntil.t_ms;
}
export function useBackupStatus(): BackupStatus | undefined {
const [status, setStatus] = useState<BackupStatus | undefined>(undefined)
const [status, setStatus] = useState<BackupStatus | undefined>(undefined);
useEffect(() => {
async function run() {
//create a first list of backup info by currency
const status = await wxApi.getBackupInfo()
const status = await wxApi.getBackupInfo();
const providers = status.providers.sort((a, b) => {
if (a.paymentStatus.type === ProviderPaymentType.Paid && b.paymentStatus.type === ProviderPaymentType.Paid) {
return getStatusPaidOrder(a.paymentStatus, b.paymentStatus)
if (
a.paymentStatus.type === ProviderPaymentType.Paid &&
b.paymentStatus.type === ProviderPaymentType.Paid
) {
return getStatusPaidOrder(a.paymentStatus, b.paymentStatus);
}
return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
})
return (
getStatusTypeOrder(a.paymentStatus) -
getStatusTypeOrder(b.paymentStatus)
);
});
async function sync() {
await wxApi.syncAllProviders()
await wxApi.syncAllProviders();
}
setStatus({ deviceName: status.deviceId, providers, sync })
setStatus({ deviceName: status.deviceId, providers, sync });
}
run()
}, [])
run();
}, []);
return status
return status;
}

View File

@ -18,7 +18,6 @@ import { BalancesResponse } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
interface BalancesHookOk {
hasError: false;
response: BalancesResponse;
@ -46,7 +45,7 @@ export function useBalances(): BalancesHook {
}
}
}
checkBalance()
checkBalance();
return wxApi.onUpdateNotification(checkBalance);
}, []);

View File

@ -21,7 +21,7 @@ import * as wxApi from "../wxApi";
export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
const [timedOut, setTimedOut] = useState(false);
const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
undefined
undefined,
);
useEffect(() => {
@ -41,5 +41,5 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
console.log("fetching diagnostics");
doFetch();
}, []);
return [diagnostics, timedOut]
}
return [diagnostics, timedOut];
}

View File

@ -19,13 +19,12 @@ import * as wxApi from "../wxApi";
import { getPermissionsApi } from "../compat";
import { extendedPermissions } from "../permissions";
export function useExtendedPermissions(): [boolean, () => void] {
const [enabled, setEnabled] = useState(false);
const toggle = () => {
setEnabled(v => !v);
handleExtendedPerm(enabled).then(result => {
setEnabled((v) => !v);
handleExtendedPerm(enabled).then((result) => {
setEnabled(result);
});
};
@ -65,5 +64,5 @@ async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {
nextVal = res.newValue;
}
console.log("new permissions applied:", nextVal ?? false);
return nextVal ?? false
}
return nextVal ?? false;
}

View File

@ -14,10 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { useNotNullLocalStorage } from './useLocalStorage';
import { useNotNullLocalStorage } from "./useLocalStorage";
export function useLang(initial?: string): [string, (s:string) => void] {
const browserLang: string | undefined = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
const defaultLang = (browserLang || initial || 'en').substring(0, 2)
return useNotNullLocalStorage('lang-preference', defaultLang)
export function useLang(initial?: string): [string, (s: string) => void] {
const browserLang: string | undefined =
typeof window !== "undefined"
? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
return useNotNullLocalStorage("lang-preference", defaultLang);
}

View File

@ -15,38 +15,52 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { StateUpdater, useState } from "preact/hooks";
export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
export function useLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>(():
| string
| undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value?: string | ((val?: string) => string | undefined)) => {
setStoredValue(p => {
const toStore = value instanceof Function ? value(p) : value
const setValue = (
value?: string | ((val?: string) => string | undefined),
) => {
setStoredValue((p) => {
const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== "undefined") {
if (!toStore) {
window.localStorage.removeItem(key)
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, toStore);
}
}
return toStore
})
return toStore;
});
};
return [storedValue, setValue];
}
//TODO: merge with the above function
export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
@ -54,7 +68,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (!valueToStore) {
window.localStorage.removeItem(key)
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, valueToStore);
}

View File

@ -32,7 +32,9 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
//create a first list of backup info by currency
const status = await wxApi.getBackupInfo();
const providers = status.providers.filter(p => p.syncProviderBaseUrl === url);
const providers = status.providers.filter(
(p) => p.syncProviderBaseUrl === url,
);
const info = providers.length ? providers[0] : undefined;
async function sync() {

View File

@ -17,15 +17,18 @@
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
export function useTalerActionURL(): [string | undefined, (s: boolean) => void] {
export function useTalerActionURL(): [
string | undefined,
(s: boolean) => void,
] {
const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
undefined
undefined,
);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
async function check(): Promise<void> {
const talerUri = await findTalerUriInActiveTab();
setTalerActionUrl(talerUri)
setTalerActionUrl(talerUri);
}
check();
}, []);

View File

@ -193,7 +193,7 @@ strings["es"] = {
"Order redirected": [""],
"Payment aborted": [""],
"Payment Sent": [""],
"Backup": ["Resguardo"],
Backup: ["Resguardo"],
"Order accepted": [""],
"Reserve balance updated": [""],
"Payment refund": [""],

View File

@ -15,179 +15,184 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { addDays } from 'date-fns';
import { BackupView as TestedComponent } from './BackupPage';
import { createExample } from '../test-utils';
import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { addDays } from "date-fns";
import { BackupView as TestedComponent } from "./BackupPage";
import { createExample } from "../test-utils";
export default {
title: 'popup/backup/list',
title: "popup/backup/list",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const LotOfProviders = createExample(TestedComponent, {
providers: [{
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": addDays(new Date(), 13).getTime()
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Pending,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.InsufficientBalance,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.TermsChanged,
newTerms: {
annualFee: 'USD:2',
storageLimitInMegabytes: 8,
supportedProtocolVersion: '2',
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
oldTerms: {
annualFee: 'USD:1',
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: '1',
supportedProtocolVersion: "0.0",
},
paidUntil: {
t_ms: 'never'
}
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: addDays(new Date(), 13).getTime(),
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}]
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
newTerms: {
annualFee: "USD:2",
storageLimitInMegabytes: 8,
supportedProtocolVersion: "2",
},
oldTerms: {
annualFee: "USD:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "1",
},
paidUntil: {
t_ms: "never",
},
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
],
});
export const OneProvider = createExample(TestedComponent, {
providers: [{
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}]
],
});
export const Empty = createExample(TestedComponent, {
providers: []
providers: [],
});

View File

@ -14,15 +14,28 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentStatus } from "@gnu-taler/taler-wallet-core";
import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
import {
ProviderInfo,
ProviderPaymentStatus,
} from "@gnu-taler/taler-wallet-core";
import {
differenceInMonths,
formatDuration,
intervalToDuration,
} from "date-fns";
import { Fragment, JSX, VNode, h } from "preact";
import {
BoldLight, ButtonPrimary, ButtonSuccess, Centered,
CenteredText, CenteredBoldText, PopupBox, RowBorderGray,
SmallText, SmallLightText
BoldLight,
ButtonPrimary,
ButtonSuccess,
Centered,
CenteredText,
CenteredBoldText,
PopupBox,
RowBorderGray,
SmallText,
SmallLightText,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "../NavigationBar";
@ -32,49 +45,68 @@ interface Props {
}
export function BackupPage({ onAddProvider }: Props): VNode {
const status = useBackupStatus()
const status = useBackupStatus();
if (!status) {
return <div>Loading...</div>
return <div>Loading...</div>;
}
return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
return (
<BackupView
providers={status.providers}
onAddProvider={onAddProvider}
onSyncAll={status.sync}
/>
);
}
export interface ViewProps {
providers: ProviderInfo[],
providers: ProviderInfo[];
onAddProvider: () => void;
onSyncAll: () => Promise<void>;
}
export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
export function BackupView({
providers,
onAddProvider,
onSyncAll,
}: ViewProps): VNode {
return (
<PopupBox>
<section>
{providers.map((provider) => <BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
active={provider.active}
title={provider.name}
/>
{providers.map((provider) => (
<BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
active={provider.active}
title={provider.name}
/>
))}
{!providers.length && (
<Centered style={{ marginTop: 100 }}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}>
<i18n.Translate>Add provider</i18n.Translate>
</ButtonSuccess>
</Centered>
)}
{!providers.length && <Centered style={{marginTop: 100}}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
</Centered>}
</section>
{!!providers.length && <footer>
<div />
<div>
<ButtonPrimary onClick={onSyncAll}>{
providers.length > 1 ?
<i18n.Translate>Sync all backups</i18n.Translate> :
<i18n.Translate>Sync now</i18n.Translate>
}</ButtonPrimary>
<ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
</div>
</footer>}
{!!providers.length && (
<footer>
<div />
<div>
<ButtonPrimary onClick={onSyncAll}>
{providers.length > 1 ? (
<i18n.Translate>Sync all backups</i18n.Translate>
) : (
<i18n.Translate>Sync now</i18n.Translate>
)}
</ButtonPrimary>
<ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
</div>
</footer>
)}
</PopupBox>
)
);
}
interface TransactionLayoutProps {
@ -92,55 +124,73 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
timeStyle: "short",
} as any);
return (
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
<a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
<a
href={Pages.provider_detail.replace(
":pid",
encodeURIComponent(props.id),
)}
>
<span>{props.title}</span>
</a>
{dateStr && <SmallText style={{marginTop: 5}}>Last synced: {dateStr}</SmallText>}
{!dateStr && <SmallLightText style={{marginTop: 5}}>Not synced</SmallLightText>}
{dateStr && (
<SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>
)}
{!dateStr && (
<SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>
)}
</div>
<div>
{props.status?.type === 'paid' ?
<ExpirationText until={props.status.paidUntil} /> :
{props.status?.type === "paid" ? (
<ExpirationText until={props.status.paidUntil} />
) : (
<div>{props.status.type}</div>
}
)}
</div>
</RowBorderGray>
);
}
function ExpirationText({ until }: { until: Timestamp }) {
return <Fragment>
<CenteredText> Expires in </CenteredText>
<CenteredBoldText {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredBoldText>
</Fragment>
return (
<Fragment>
<CenteredText> Expires in </CenteredText>
<CenteredBoldText {...{ color: colorByTimeToExpire(until) }}>
{" "}
{daysUntil(until)}{" "}
</CenteredBoldText>
</Fragment>
);
}
function colorByTimeToExpire(d: Timestamp) {
if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
const months = differenceInMonths(d.t_ms, new Date())
return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
if (d.t_ms === "never") return "rgb(28, 184, 65)";
const months = differenceInMonths(d.t_ms, new Date());
return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)";
}
function daysUntil(d: Timestamp) {
if (d.t_ms === 'never') return undefined
if (d.t_ms === "never") return undefined;
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
})
});
const str = formatDuration(duration, {
delimiter: ', ',
delimiter: ", ",
format: [
duration?.years ? 'years' : (
duration?.months ? 'months' : (
duration?.days ? 'days' : (
duration.hours ? 'hours' : 'minutes'
)
)
)
]
})
return `${str}`
}
duration?.years
? "years"
: duration?.months
? "months"
: duration?.days
? "days"
: duration.hours
? "hours"
: "minutes",
],
});
return `${str}`;
}

View File

@ -15,28 +15,25 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, NullLink } from '../test-utils';
import { BalanceView as TestedComponent } from './BalancePage';
import { createExample, NullLink } from "../test-utils";
import { BalanceView as TestedComponent } from "./BalancePage";
export default {
title: 'popup/balance',
title: "popup/balance",
component: TestedComponent,
argTypes: {
}
argTypes: {},
};
export const NotYetLoaded = createExample(TestedComponent, {
});
export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
message: 'Network error'
message: "Network error",
},
Linker: NullLink,
});
@ -45,7 +42,7 @@ export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: []
balances: [],
},
},
Linker: NullLink,
@ -55,13 +52,15 @@ export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:10.5',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -71,13 +70,15 @@ export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2.23',
hasPendingTransactions: false,
pendingIncoming: 'USD:5.11',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:5.11",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -87,13 +88,15 @@ export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2.23',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:5.11',
requiresUserInput: false
}]
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:5.11",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -103,13 +106,15 @@ export const SomeCoinsAndMovingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2.23',
hasPendingTransactions: false,
pendingIncoming: 'USD:2',
pendingOutgoing: 'USD:5.11',
requiresUserInput: false
}]
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:2",
pendingOutgoing: "USD:5.11",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -119,19 +124,22 @@ export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2',
hasPendingTransactions: false,
pendingIncoming: 'USD:5.1',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'EUR:4',
hasPendingTransactions: false,
pendingIncoming: 'EUR:0',
pendingOutgoing: 'EUR:3.01',
requiresUserInput: false
}]
balances: [
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:5.1",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:3.01",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -141,78 +149,89 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:1',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'COL:2000',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'EUR:4',
hasPendingTransactions: false,
pendingIncoming: 'EUR:15',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:1",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "COL:2000",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:15",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
});
export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:13451',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'EUR:202.02',
hasPendingTransactions: false,
pendingIncoming: 'EUR:0',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
},{
available: 'ARS:30',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'JPY:51223233',
hasPendingTransactions: false,
pendingIncoming: 'EUR:0',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
},{
available: 'JPY:51223233',
hasPendingTransactions: false,
pendingIncoming: 'EUR:0',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
},{
available: 'DEMOKUDOS:6',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'TESTKUDOS:6',
hasPendingTransactions: false,
pendingIncoming: 'USD:5',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:13451",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:202.02",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "ARS:30",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "JPY:51223233",
hasPendingTransactions: false,
pendingIncoming: "EUR:0",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
{
available: "DEMOKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "TESTKUDOS:6",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,

View File

@ -15,20 +15,37 @@
*/
import {
amountFractionalBase, Amounts,
Balance, BalancesResponse,
i18n
amountFractionalBase,
Amounts,
Balance,
BalancesResponse,
i18n,
} from "@gnu-taler/taler-util";
import { JSX, h, Fragment } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
import { PopupBox, Centered, ButtonPrimary, ErrorBox, Middle } from "../components/styled/index";
import {
PopupBox,
Centered,
ButtonPrimary,
ErrorBox,
Middle,
} from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
const balance = useBalances()
return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
export function BalancePage({
goToWalletManualWithdraw,
}: {
goToWalletManualWithdraw: () => void;
}) {
const balance = useBalances();
return (
<BalanceView
balance={balance}
Linker={PageLink}
goToWalletManualWithdraw={goToWalletManualWithdraw}
/>
);
}
export interface BalanceViewProps {
balance: BalancesHook;
@ -46,22 +63,26 @@ function formatPending(entry: Balance): JSX.Element {
if (!Amounts.isZero(pendingIncoming)) {
incoming = (
<span><i18n.Translate>
<span style={{ color: "darkgreen" }} title="incoming amount">
{"+"}
{renderAmount(entry.pendingIncoming)}
</span>{" "}
</i18n.Translate></span>
<span>
<i18n.Translate>
<span style={{ color: "darkgreen" }} title="incoming amount">
{"+"}
{renderAmount(entry.pendingIncoming)}
</span>{" "}
</i18n.Translate>
</span>
);
}
if (!Amounts.isZero(pendingOutgoing)) {
payment = (
<span><i18n.Translate>
<span style={{ color: "darkred" }} title="outgoing amount">
{"-"}
{renderAmount(entry.pendingOutgoing)}
</span>{" "}
</i18n.Translate></span>
<span>
<i18n.Translate>
<span style={{ color: "darkred" }} title="outgoing amount">
{"-"}
{renderAmount(entry.pendingOutgoing)}
</span>{" "}
</i18n.Translate>
</span>
);
}
@ -80,76 +101,110 @@ function formatPending(entry: Balance): JSX.Element {
);
}
export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
}: BalanceViewProps) {
function Content() {
if (!balance) {
return <span />
return <span />;
}
if (balance.hasError) {
return (<section>
<ErrorBox>{balance.message}</ErrorBox>
<p>
Click <Linker pageName="welcome">here</Linker> for help and
diagnostics.
</p>
</section>)
return (
<section>
<ErrorBox>{balance.message}</ErrorBox>
<p>
Click <Linker pageName="welcome">here</Linker> for help and
diagnostics.
</p>
</section>
);
}
if (balance.response.balances.length === 0) {
return (<section data-expanded>
<Middle>
<p><i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
</i18n.Translate></p>
</Middle>
</section>)
return (
<section data-expanded>
<Middle>
<p>
<i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
</i18n.Translate>
</p>
</Middle>
</section>
);
}
return <section data-expanded data-centered>
<table style={{width:'100%'}}>{balance.response.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
// Create our number formatter.
let formatter;
try {
formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: av.currency,
currencyDisplay: 'symbol'
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
});
} catch {
formatter = new Intl.NumberFormat('en-US', {
// style: 'currency',
// currency: av.currency,
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
});
}
return (
<section data-expanded data-centered>
<table style={{ width: "100%" }}>
{balance.response.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
// Create our number formatter.
let formatter;
try {
formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: av.currency,
currencyDisplay: "symbol",
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
});
} catch {
formatter = new Intl.NumberFormat("en-US", {
// style: 'currency',
// currency: av.currency,
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
});
}
const v = formatter.format(av.value + av.fraction / amountFractionalBase);
const fontSize = v.length < 8 ? '3em' : (v.length < 13 ? '2em' : '1em')
return (<tr>
<td style={{ height: 50, fontSize, width: '60%', textAlign: 'right', padding: 0 }}>{v}</td>
<td style={{ maxWidth: '2em', overflowX: 'hidden' }}>{av.currency}</td>
<td style={{ fontSize: 'small', color: 'gray' }}>{formatPending(entry)}</td>
</tr>
);
})}</table>
</section>
const v = formatter.format(
av.value + av.fraction / amountFractionalBase,
);
const fontSize =
v.length < 8 ? "3em" : v.length < 13 ? "2em" : "1em";
return (
<tr>
<td
style={{
height: 50,
fontSize,
width: "60%",
textAlign: "right",
padding: 0,
}}
>
{v}
</td>
<td style={{ maxWidth: "2em", overflowX: "hidden" }}>
{av.currency}
</td>
<td style={{ fontSize: "small", color: "gray" }}>
{formatPending(entry)}
</td>
</tr>
);
})}
</table>
</section>
);
}
return <PopupBox>
{/* <section> */}
<Content />
{/* </section> */}
<footer>
<div />
<ButtonPrimary onClick={goToWalletManualWithdraw}>Withdraw</ButtonPrimary>
</footer>
</PopupBox>
return (
<PopupBox>
{/* <section> */}
<Content />
{/* </section> */}
<footer>
<div />
<ButtonPrimary onClick={goToWalletManualWithdraw}>
Withdraw
</ButtonPrimary>
</footer>
</PopupBox>
);
}

View File

@ -19,13 +19,14 @@ import { Diagnostics } from "../components/Diagnostics";
import { useDiagnostics } from "../hooks/useDiagnostics.js";
import * as wxApi from "../wxApi";
export function DeveloperPage(props: any): JSX.Element {
const [status, timedOut] = useDiagnostics();
return (
<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button>
<button onClick={openExtensionPage("/static/popup.html")}>
wallet tab
</button>
<br />
<button onClick={confirmReset}>reset</button>
<Diagnostics diagnostics={status} timedOut={timedOut} />
@ -46,7 +47,7 @@ export async function confirmReset(): Promise<void> {
if (
confirm(
"Do you want to IRREVOCABLY DESTROY everything inside your" +
" wallet and LOSE ALL YOUR COINS?",
" wallet and LOSE ALL YOUR COINS?",
)
) {
await wxApi.resetDb();
@ -61,4 +62,3 @@ export function openExtensionPage(page: string) {
});
};
}

View File

@ -15,135 +15,149 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
PaymentStatus,
TransactionCommon, TransactionDeposit, TransactionPayment,
TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalType
} from '@gnu-taler/taler-util';
import { createExample } from '../test-utils';
import { HistoryView as TestedComponent } from './History';
WithdrawalType,
} from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { HistoryView as TestedComponent } from "./History";
export default {
title: 'popup/history/list',
title: "popup/history/list",
component: TestedComponent,
};
const commonTransaction = {
amountRaw: 'USD:10',
amountEffective: 'USD:9',
amountRaw: "USD:10",
amountEffective: "USD:9",
pending: false,
timestamp: {
t_ms: new Date().getTime()
t_ms: new Date().getTime(),
},
transactionId: '12',
} as TransactionCommon
transactionId: "12",
} as TransactionCommon;
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
exchangeBaseUrl: 'http://exchange.demo.taler.net',
exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
confirmed: false,
exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
}
},
} as TransactionWithdrawal,
payment: {
...commonTransaction,
amountEffective: 'USD:11',
amountEffective: "USD:11",
type: TransactionType.Payment,
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'the merchant',
name: "the merchant",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: 'the summary',
fulfillmentMessage: '',
summary: "the summary",
fulfillmentMessage: "",
},
proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
deposit: {
...commonTransaction,
type: TransactionType.Deposit,
depositGroupId: '#groupId',
targetPaytoUri: 'payto://x-taler-bank/bank/account',
depositGroupId: "#groupId",
targetPaytoUri: "payto://x-taler-bank/bank/account",
} as TransactionDeposit,
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
exchangeBaseUrl: 'http://exchange.taler',
exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
merchantBaseUrl: 'http://merchant.taler',
merchantBaseUrl: "http://merchant.taler",
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'the merchant',
name: "the merchant",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: 'the summary',
fulfillmentMessage: '',
summary: "the summary",
fulfillmentMessage: "",
},
} as TransactionRefund,
}
};
export const EmptyWithBalance = createExample(TestedComponent, {
list: [],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const EmptyWithNoBalance = createExample(TestedComponent, {
list: [],
balances: []
balances: [],
});
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
balances: [{
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const OnePending = createExample(TestedComponent, {
list: [{
...exampleData.withdraw,
pending: true,
}],
balances: [{
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
list: [
{
...exampleData.withdraw,
pending: true,
},
],
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const Several = createExample(TestedComponent, {
@ -157,13 +171,15 @@ export const Several = createExample(TestedComponent, {
exampleData.tip,
exampleData.deposit,
],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
@ -177,18 +193,20 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
exampleData.tip,
exampleData.deposit,
],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}, {
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});

View File

@ -14,7 +14,13 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountString, Balance, i18n, Transaction, TransactionsResponse } from "@gnu-taler/taler-util";
import {
AmountString,
Balance,
i18n,
Transaction,
TransactionsResponse,
} from "@gnu-taler/taler-util";
import { h, JSX } from "preact";
import { useEffect, useState } from "preact/hooks";
import { PopupBox } from "../components/styled";
@ -22,13 +28,14 @@ import { TransactionItem } from "../components/TransactionItem";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
export function HistoryPage(props: any): JSX.Element {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
const balance = useBalances()
const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
const balance = useBalances();
const balanceWithoutError = balance?.hasError
? []
: balance?.response.balances || [];
useEffect(() => {
const fetchData = async (): Promise<void> => {
@ -42,46 +49,79 @@ export function HistoryPage(props: any): JSX.Element {
return <div>Loading ...</div>;
}
return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
return (
<HistoryView
balances={balanceWithoutError}
list={[...transactions.transactions].reverse()}
/>
);
}
function amountToString(c: AmountString) {
const idx = c.indexOf(':')
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`
const idx = c.indexOf(":");
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
}
export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) {
const multiCurrency = balances.length > 1
return <PopupBox noPadding>
{balances.length > 0 && <header>
{multiCurrency ? <div class="title">
Balance: <ul style={{ margin: 0 }}>
{balances.map(b => <li>{b.available}</li>)}
</ul>
</div> : <div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>}
</header>}
{list.length === 0 ? <section data-expanded data-centered>
<p><i18n.Translate>
You have no history yet, here you will be able to check your last transactions.
</i18n.Translate></p>
</section> :
<section>
{list.slice(0, 3).map((tx, i) => (
<TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} />
))}
</section>
}
<footer style={{ justifyContent: 'space-around' }}>
{list.length > 0 &&
<a target="_blank"
rel="noopener noreferrer"
style={{ color: 'darkgreen', textDecoration: 'none' }}
href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a>
}
</footer>
</PopupBox>
export function HistoryView({
list,
balances,
}: {
list: Transaction[];
balances: Balance[];
}) {
const multiCurrency = balances.length > 1;
return (
<PopupBox noPadding>
{balances.length > 0 && (
<header>
{multiCurrency ? (
<div class="title">
Balance:{" "}
<ul style={{ margin: 0 }}>
{balances.map((b) => (
<li>{b.available}</li>
))}
</ul>
</div>
) : (
<div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>
)}
</header>
)}
{list.length === 0 ? (
<section data-expanded data-centered>
<p>
<i18n.Translate>
You have no history yet, here you will be able to check your last
transactions.
</i18n.Translate>
</p>
</section>
) : (
<section>
{list.slice(0, 3).map((tx, i) => (
<TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} />
))}
</section>
)}
<footer style={{ justifyContent: "space-around" }}>
{list.length > 0 && (
<a
target="_blank"
rel="noopener noreferrer"
style={{ color: "darkgreen", textDecoration: "none" }}
href={
chrome.extension
? chrome.extension.getURL(`/static/wallet.html#/history`)
: "#"
}
>
VIEW MORE TRANSACTIONS
</a>
)}
</footer>
</PopupBox>
);
}

View File

@ -15,30 +15,29 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { NavBar as TestedComponent } from '../NavigationBar';
import { createExample } from "../test-utils";
import { NavBar as TestedComponent } from "../NavigationBar";
export default {
title: 'popup/header',
title: "popup/header",
// component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const OnBalance = createExample(TestedComponent, {
devMode:false,
path:'/balance'
devMode: false,
path: "/balance",
});
export const OnHistoryWithDevMode = createExample(TestedComponent, {
devMode:true,
path:'/history'
devMode: true,
path: "/history",
});

View File

@ -15,38 +15,37 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { ConfirmProviderView as TestedComponent } from './ProviderAddPage';
import { createExample } from "../test-utils";
import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage";
export default {
title: 'popup/backup/confirm',
title: "popup/backup/confirm",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const DemoService = createExample(TestedComponent, {
url: 'https://sync.demo.taler.net/',
url: "https://sync.demo.taler.net/",
provider: {
annual_fee: 'KUDOS:0.1',
storage_limit_in_megabytes: 20,
supported_protocol_version: '1'
}
annual_fee: "KUDOS:0.1",
storage_limit_in_megabytes: 20,
supported_protocol_version: "1",
},
});
export const FreeService = createExample(TestedComponent, {
url: 'https://sync.taler:9667/',
url: "https://sync.taler:9667/",
provider: {
annual_fee: 'ARS:0',
storage_limit_in_megabytes: 20,
supported_protocol_version: '1'
}
annual_fee: "ARS:0",
storage_limit_in_megabytes: 20,
supported_protocol_version: "1",
},
});

View File

@ -15,39 +15,37 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { SetUrlView as TestedComponent } from './ProviderAddPage';
import { createExample } from "../test-utils";
import { SetUrlView as TestedComponent } from "./ProviderAddPage";
export default {
title: 'popup/backup/add',
title: "popup/backup/add",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const Initial = createExample(TestedComponent, {
});
export const Initial = createExample(TestedComponent, {});
export const WithValue = createExample(TestedComponent, {
initialValue: 'sync.demo.taler.net'
});
initialValue: "sync.demo.taler.net",
});
export const WithConnectionError = createExample(TestedComponent, {
withError: 'Network error'
});
withError: "Network error",
});
export const WithClientError = createExample(TestedComponent, {
withError: 'URL may not be right: (404) Not Found'
});
withError: "URL may not be right: (404) Not Found",
});
export const WithServerError = createExample(TestedComponent, {
withError: 'Try another server: (500) Internal Server Error'
});
withError: "Try another server: (500) Internal Server Error",
});

View File

@ -15,224 +15,221 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { createExample } from '../test-utils';
import { ProviderView as TestedComponent } from './ProviderDetailPage';
import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { createExample } from "../test-utils";
import { ProviderView as TestedComponent } from "./ProviderDetailPage";
export default {
title: 'popup/backup/details',
title: "popup/backup/details",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const Active = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveErrorSync = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
lastAttemptedBackupTimestamp: {
"t_ms": 1625063925078
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
lastError: {
code: 2002,
details: 'details',
hint: 'error hint from the server',
message: 'message'
details: "details",
hint: "error hint from the server",
message: "message",
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
backupProblem: {
type: 'backup-unreadable'
type: "backup-unreadable",
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveBackupProblemDevice = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
backupProblem: {
type: 'backup-conflicting-device',
myDeviceId: 'my-device-id',
otherDeviceId: 'other-device-id',
type: "backup-conflicting-device",
myDeviceId: "my-device-id",
otherDeviceId: "other-device-id",
backupTimestamp: {
"t_ms": 1656599921000
}
t_ms: 1656599921000,
},
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactiveUnpaid = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactiveInsufficientBalance = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.InsufficientBalance,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactivePending = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Pending,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveTermsChanged = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.TermsChanged,
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
paidUntil: {
t_ms: 1656599921000
t_ms: 1656599921000,
},
newTerms: {
"annualFee": "EUR:10",
"storageLimitInMegabytes": 8,
"supportedProtocolVersion": "0.0"
annualFee: "EUR:10",
storageLimitInMegabytes: 8,
supportedProtocolVersion: "0.0",
},
oldTerms: {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});

View File

@ -14,13 +14,23 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import {
ProviderInfo,
ProviderPaymentStatus,
ProviderPaymentType,
} from "@gnu-taler/taler-wallet-core";
import { format, formatDuration, intervalToDuration } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, PopupBox, SmallLightText } from "../components/styled";
import {
Button,
ButtonDestructive,
ButtonPrimary,
PaymentStatus,
PopupBox,
SmallLightText,
} from "../components/styled";
import { useProviderStatus } from "../hooks/useProviderStatus";
interface Props {
@ -29,20 +39,29 @@ interface Props {
}
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
const status = useProviderStatus(pid)
const status = useProviderStatus(pid);
if (!status) {
return <div><i18n.Translate>Loading...</i18n.Translate></div>
return (
<div>
<i18n.Translate>Loading...</i18n.Translate>
</div>
);
}
if (!status.info) {
onBack()
return <div />
onBack();
return <div />;
}
return <ProviderView info={status.info}
onSync={status.sync}
onDelete={() => status.remove().then(onBack)}
onBack={onBack}
onExtend={() => { null }}
/>;
return (
<ProviderView
info={status.info}
onSync={status.sync}
onDelete={() => status.remove().then(onBack)}
onBack={onBack}
onExtend={() => {
null;
}}
/>
);
}
export interface ViewProps {
@ -53,124 +72,185 @@ export interface ViewProps {
onExtend: () => void;
}
export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode {
const lb = info?.lastSuccessfulBackupTimestamp
const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged
export function ProviderView({
info,
onDelete,
onSync,
onBack,
onExtend,
}: ViewProps): VNode {
const lb = info?.lastSuccessfulBackupTimestamp;
const isPaid =
info.paymentStatus.type === ProviderPaymentType.Paid ||
info.paymentStatus.type === ProviderPaymentType.TermsChanged;
return (
<PopupBox>
<Error info={info} />
<header>
<h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3>
<PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus>
<h3>
{info.name}{" "}
<SmallLightText>{info.syncProviderBaseUrl}</SmallLightText>
</h3>
<PaymentStatus color={isPaid ? "rgb(28, 184, 65)" : "rgb(202, 60, 60)"}>
{isPaid ? "Paid" : "Unpaid"}
</PaymentStatus>
</header>
<section>
<p><b>Last backup:</b> {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')} </p>
<ButtonPrimary onClick={onSync}><i18n.Translate>Back up</i18n.Translate></ButtonPrimary>
{info.terms && <Fragment>
<p><b>Provider fee:</b> {info.terms && info.terms.annualFee} per year</p>
</Fragment>
}
<p>
<b>Last backup:</b>{" "}
{lb == null || lb.t_ms == "never"
? "never"
: format(lb.t_ms, "dd MMM yyyy")}{" "}
</p>
<ButtonPrimary onClick={onSync}>
<i18n.Translate>Back up</i18n.Translate>
</ButtonPrimary>
{info.terms && (
<Fragment>
<p>
<b>Provider fee:</b> {info.terms && info.terms.annualFee} per year
</p>
</Fragment>
)}
<p>{descriptionByStatus(info.paymentStatus)}</p>
<ButtonPrimary disabled onClick={onExtend}><i18n.Translate>Extend</i18n.Translate></ButtonPrimary>
{info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
<p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p>
<table>
<thead>
<tr>
<td></td>
<td><i18n.Translate>old</i18n.Translate></td>
<td> -&gt;</td>
<td><i18n.Translate>new</i18n.Translate></td>
</tr>
</thead>
<tbody>
<tr>
<td><i18n.Translate>fee</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td><i18n.Translate>storage</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>}
<ButtonPrimary disabled onClick={onExtend}>
<i18n.Translate>Extend</i18n.Translate>
</ButtonPrimary>
{info.paymentStatus.type === ProviderPaymentType.TermsChanged && (
<div>
<p>
<i18n.Translate>
terms has changed, extending the service will imply accepting
the new terms of service
</i18n.Translate>
</p>
<table>
<thead>
<tr>
<td></td>
<td>
<i18n.Translate>old</i18n.Translate>
</td>
<td> -&gt;</td>
<td>
<i18n.Translate>new</i18n.Translate>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<i18n.Translate>fee</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td>
<i18n.Translate>storage</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>
)}
</section>
<footer>
<Button onClick={onBack}><i18n.Translate> &lt; back</i18n.Translate></Button>
<Button onClick={onBack}>
<i18n.Translate> &lt; back</i18n.Translate>
</Button>
<div>
<ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
<ButtonDestructive onClick={onDelete}>
<i18n.Translate>remove provider</i18n.Translate>
</ButtonDestructive>
</div>
</footer>
</PopupBox>
)
);
}
function daysSince(d?: Timestamp) {
if (!d || d.t_ms === 'never') return 'never synced'
if (!d || d.t_ms === "never") return "never synced";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
})
});
const str = formatDuration(duration, {
delimiter: ', ',
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`
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 }) {
if (info.lastError) {
return <ErrorMessage title={info.lastError.hint} />
return <ErrorMessage title={info.lastError.hint} />;
}
if (info.backupProblem) {
switch (info.backupProblem.type) {
case "backup-conflicting-device":
return <ErrorMessage title={<Fragment>
<i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
</Fragment>} />
return (
<ErrorMessage
title={
<Fragment>
<i18n.Translate>
There is conflict with another backup from{" "}
<b>{info.backupProblem.otherDeviceId}</b>
</i18n.Translate>
</Fragment>
}
/>
);
case "backup-unreadable":
return <ErrorMessage title="Backup is not readable" />
return <ErrorMessage title="Backup is not readable" />;
default:
return <ErrorMessage title={<Fragment>
<i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate>
</Fragment>} />
return (
<ErrorMessage
title={
<Fragment>
<i18n.Translate>
Unknown backup problem: {JSON.stringify(info.backupProblem)}
</i18n.Translate>
</Fragment>
}
/>
);
}
}
return null
return null;
}
function colorByStatus(status: ProviderPaymentType) {
switch (status) {
case ProviderPaymentType.InsufficientBalance:
return 'rgb(223, 117, 20)'
return "rgb(223, 117, 20)";
case ProviderPaymentType.Unpaid:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
case ProviderPaymentType.Paid:
return 'rgb(28, 184, 65)'
return "rgb(28, 184, 65)";
case ProviderPaymentType.Pending:
return 'gray'
return "gray";
case ProviderPaymentType.InsufficientBalance:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
case ProviderPaymentType.TermsChanged:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
}
}
@ -180,16 +260,19 @@ function descriptionByStatus(status: ProviderPaymentStatus) {
// return i18n.str`not paid yet`
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
if (status.paidUntil.t_ms === 'never') {
return i18n.str`service paid`
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 (
<Fragment>
<b>Backup valid until:</b>{" "}
{format(status.paidUntil.t_ms, "dd MMM yyyy")}
</Fragment>
);
}
case ProviderPaymentType.Unpaid:
case ProviderPaymentType.InsufficientBalance:
case ProviderPaymentType.Pending:
return ''
return "";
}
}

View File

@ -15,29 +15,28 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { SettingsView as TestedComponent } from './Settings';
import { createExample } from "../test-utils";
import { SettingsView as TestedComponent } from "./Settings";
export default {
title: 'popup/settings',
title: "popup/settings",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
}
},
};
export const AllOff = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
deviceName: "this-is-the-device-name",
setDeviceName: () => Promise.resolve(),
});
export const OneChecked = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
});

View File

@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { Checkbox } from "../components/Checkbox";
@ -28,15 +27,21 @@ import { useLang } from "../hooks/useLang";
export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const { devMode, toggleDevMode } = useDevContext()
const { name, update } = useBackupDeviceName()
const [lang, changeLang] = useLang()
return <SettingsView
lang={lang} changeLang={changeLang}
deviceName={name} setDeviceName={update}
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
developerMode={devMode} toggleDeveloperMode={toggleDevMode}
/>;
const { devMode, toggleDevMode } = useDevContext();
const { name, update } = useBackupDeviceName();
const [lang, changeLang] = useLang();
return (
<SettingsView
lang={lang}
changeLang={changeLang}
deviceName={name}
setDeviceName={update}
permissionsEnabled={permissionsEnabled}
togglePermissions={togglePermissions}
developerMode={devMode}
toggleDeveloperMode={toggleDevMode}
/>
);
}
export interface ViewProps {
@ -50,23 +55,31 @@ export interface ViewProps {
toggleDeveloperMode: () => void;
}
import { strings as messages } from '../i18n/strings'
import { strings as messages } from "../i18n/strings";
type LangsNames = {
[P in keyof typeof messages]: string
}
[P in keyof typeof messages]: string;
};
const names: LangsNames = {
es: 'Español [es]',
en: 'English [en]',
fr: 'Français [fr]',
de: 'Deutsch [de]',
sv: 'Svenska [sv]',
it: 'Italiano [it]',
}
es: "Español [es]",
en: "English [en]",
fr: "Français [fr]",
de: "Deutsch [de]",
sv: "Svenska [sv]",
it: "Italiano [it]",
};
export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
export function SettingsView({
lang,
changeLang,
deviceName,
setDeviceName,
permissionsEnabled,
togglePermissions,
developerMode,
toggleDeveloperMode,
}: ViewProps): VNode {
return (
<PopupBox>
<section>
@ -86,25 +99,39 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm
label={i18n.str`Device name`}
description="(This is how you will recognize the wallet in the backup provider)"
/> */}
<h2><i18n.Translate>Permissions</i18n.Translate></h2>
<Checkbox label="Automatically open wallet based on page content"
<h2>
<i18n.Translate>Permissions</i18n.Translate>
</h2>
<Checkbox
label="Automatically open wallet based on page content"
name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
enabled={permissionsEnabled} onToggle={togglePermissions}
enabled={permissionsEnabled}
onToggle={togglePermissions}
/>
<h2>Config</h2>
<Checkbox label="Developer mode"
<Checkbox
label="Developer mode"
name="devMode"
description="(More options and information useful for debugging)"
enabled={developerMode} onToggle={toggleDeveloperMode}
enabled={developerMode}
onToggle={toggleDeveloperMode}
/>
</section>
<footer style={{ justifyContent: 'space-around' }}>
<a target="_blank"
<footer style={{ justifyContent: "space-around" }}>
<a
target="_blank"
rel="noopener noreferrer"
style={{ color: 'darkgreen', textDecoration: 'none' }}
href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/settings`) : '#'}>VIEW MORE SETTINGS</a>
style={{ color: "darkgreen", textDecoration: "none" }}
href={
chrome.extension
? chrome.extension.getURL(`/static/wallet.html#/settings`)
: "#"
}
>
VIEW MORE SETTINGS
</a>
</footer>
</PopupBox>
)
}
);
}

View File

@ -15,38 +15,38 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { TalerActionFound as TestedComponent } from './TalerActionFound';
import { createExample } from "../test-utils";
import { TalerActionFound as TestedComponent } from "./TalerActionFound";
export default {
title: 'popup/TalerActionFound',
title: "popup/TalerActionFound",
component: TestedComponent,
};
export const PayAction = createExample(TestedComponent, {
url: 'taler://pay/something'
url: "taler://pay/something",
});
export const WithdrawalAction = createExample(TestedComponent, {
url: 'taler://withdraw/something'
url: "taler://withdraw/something",
});
export const TipAction = createExample(TestedComponent, {
url: 'taler://tip/something'
url: "taler://tip/something",
});
export const NotifyAction = createExample(TestedComponent, {
url: 'taler://notify-reserve/something'
url: "taler://notify-reserve/something",
});
export const RefundAction = createExample(TestedComponent, {
url: 'taler://refund/something'
url: "taler://refund/something",
});
export const InvalidAction = createExample(TestedComponent, {
url: 'taler://something/asd'
url: "taler://something/asd",
});

View File

@ -1,5 +1,31 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
import { ButtonPrimary, ButtonSuccess, PopupBox } from "../components/styled/index";
import {
ButtonPrimary,
ButtonSuccess,
PopupBox,
} from "../components/styled/index";
import { h } from "preact";
export interface Props {
url: string;
@ -8,54 +34,89 @@ export interface Props {
export function TalerActionFound({ url, onDismiss }: Props) {
const uriType = classifyTalerUri(url);
return <PopupBox>
<section>
<h1>Taler Action </h1>
{uriType === TalerUriType.TalerPay && <div>
<p>This page has pay action.</p>
<ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
Open pay page
</ButtonSuccess>
</div>}
{uriType === TalerUriType.TalerWithdraw && <div>
<p>This page has a withdrawal action.</p>
<ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
Open withdraw page
</ButtonSuccess>
</div>}
{uriType === TalerUriType.TalerTip && <div>
<p>This page has a tip action.</p>
<ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
Open tip page
</ButtonSuccess>
</div>}
{uriType === TalerUriType.TalerNotifyReserve && <div>
<p>This page has a notify reserve action.</p>
<ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
Notify
</ButtonSuccess>
</div>}
{uriType === TalerUriType.TalerRefund && <div>
<p>This page has a refund action.</p>
<ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
Open refund page
</ButtonSuccess>
</div>}
{uriType === TalerUriType.Unknown && <div>
<p>This page has a malformed taler uri.</p>
<p>{url}</p>
</div>}
</section>
<footer>
<div />
<ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary>
</footer>
</PopupBox>;
return (
<PopupBox>
<section>
<h1>Taler Action </h1>
{uriType === TalerUriType.TalerPay && (
<div>
<p>This page has pay action.</p>
<ButtonSuccess
onClick={() => {
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
}}
>
Open pay page
</ButtonSuccess>
</div>
)}
{uriType === TalerUriType.TalerWithdraw && (
<div>
<p>This page has a withdrawal action.</p>
<ButtonSuccess
onClick={() => {
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
}}
>
Open withdraw page
</ButtonSuccess>
</div>
)}
{uriType === TalerUriType.TalerTip && (
<div>
<p>This page has a tip action.</p>
<ButtonSuccess
onClick={() => {
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
}}
>
Open tip page
</ButtonSuccess>
</div>
)}
{uriType === TalerUriType.TalerNotifyReserve && (
<div>
<p>This page has a notify reserve action.</p>
<ButtonSuccess
onClick={() => {
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
}}
>
Notify
</ButtonSuccess>
</div>
)}
{uriType === TalerUriType.TalerRefund && (
<div>
<p>This page has a refund action.</p>
<ButtonSuccess
onClick={() => {
chrome.tabs.create({ url: actionForTalerUri(uriType, url) });
}}
>
Open refund page
</ButtonSuccess>
</div>
)}
{uriType === TalerUriType.Unknown && (
<div>
<p>This page has a malformed taler uri.</p>
<p>{url}</p>
</div>
)}
</section>
<footer>
<div />
<ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary>
</footer>
</PopupBox>
);
}
function actionForTalerUri(uriType: TalerUriType, talerUri: string): string | undefined {
function actionForTalerUri(
uriType: TalerUriType,
talerUri: string,
): string | undefined {
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
@ -91,8 +152,10 @@ function makeExtensionUrlWithParams(
): string {
const innerUrl = new URL(chrome.extension.getURL("/" + url));
if (params) {
const hParams = Object.keys(params).map(k => `${k}=${params[k]}`).join('&')
innerUrl.hash = innerUrl.hash + '?' + hParams
const hParams = Object.keys(params)
.map((k) => `${k}=${params[k]}`)
.join("&");
innerUrl.hash = innerUrl.hash + "?" + hParams;
}
return innerUrl.href;
}

View File

@ -22,19 +22,17 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
import { render, h, VNode } from "preact";
import Router, { route, Route, getCurrentUrl } from "preact-router";
import { useEffect, useState } from "preact/hooks";
import { render, h } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
import { DevContextProvider } from "./context/devContext";
import { useTalerActionURL } from "./hooks/useTalerActionURL";
import { strings } from "./i18n/strings";
import { Pages, WalletNavBar } from "./NavigationBar";
import { BackupPage } from "./popup/BackupPage";
import { BalancePage } from "./popup/BalancePage";
import { DeveloperPage as DeveloperPage } from "./popup/Debug";
import { DeveloperPage } from "./popup/Debug";
import { HistoryPage } from "./popup/History";
import {
Pages, WalletNavBar
} from "./NavigationBar";
import { ProviderAddPage } from "./popup/ProviderAddPage";
import { ProviderDetailPage } from "./popup/ProviderDetailPage";
import { SettingsPage } from "./popup/Settings";
@ -64,11 +62,11 @@ if (document.readyState === "loading") {
}
function Application() {
const [talerActionUrl, setDismissed] = useTalerActionURL()
const [talerActionUrl, setDismissed] = useTalerActionURL();
useEffect(() => {
if (talerActionUrl) route(Pages.cta)
},[talerActionUrl])
if (talerActionUrl) route(Pages.cta);
}, [talerActionUrl]);
return (
<div>
@ -78,33 +76,54 @@ function Application() {
<Router history={createHashHistory()}>
<Route path={Pages.dev} component={DeveloperPage} />
<Route path={Pages.balance} component={BalancePage}
goToWalletManualWithdraw={() => goToWalletPage(Pages.manual_withdraw)}
<Route
path={Pages.balance}
component={BalancePage}
goToWalletManualWithdraw={() =>
goToWalletPage(Pages.manual_withdraw)
}
/>
<Route path={Pages.settings} component={SettingsPage} />
<Route path={Pages.cta} component={() => <TalerActionFound url={talerActionUrl!} onDismiss={() => {
setDismissed(true)
route(Pages.balance)
}} />} />
<Route
path={Pages.cta}
component={() => (
<TalerActionFound
url={talerActionUrl!}
onDismiss={() => {
setDismissed(true);
route(Pages.balance);
}}
/>
)}
/>
<Route path={Pages.transaction}
component={({ tid }: { tid: string }) => goToWalletPage(Pages.transaction.replace(':tid', tid))}
<Route
path={Pages.transaction}
component={({ tid }: { tid: string }) =>
goToWalletPage(Pages.transaction.replace(":tid", tid))
}
/>
<Route path={Pages.history} component={HistoryPage} />
<Route path={Pages.backup} component={BackupPage}
<Route
path={Pages.backup}
component={BackupPage}
onAddProvider={() => {
route(Pages.provider_add)
route(Pages.provider_add);
}}
/>
<Route path={Pages.provider_detail} component={ProviderDetailPage}
<Route
path={Pages.provider_detail}
component={ProviderDetailPage}
onBack={() => {
route(Pages.backup)
route(Pages.backup);
}}
/>
<Route path={Pages.provider_add} component={ProviderAddPage}
<Route
path={Pages.provider_add}
component={ProviderAddPage}
onBack={() => {
route(Pages.backup)
route(Pages.backup);
}}
/>
<Route default component={Redirect} to={Pages.balance} />
@ -119,13 +138,13 @@ function goToWalletPage(page: Pages | string): null {
chrome.tabs.create({
active: true,
url: chrome.extension.getURL(`/static/wallet.html#${page}`),
})
return null
});
return null;
}
function Redirect({ to }: { to: string }): null {
useEffect(() => {
route(to, true)
})
return null
route(to, true);
});
return null;
}

View File

@ -87,10 +87,7 @@ interface CollapsibleProps {
* Component that shows/hides its children when clicking
* a heading.
*/
export class Collapsible extends Component<
CollapsibleProps,
CollapsibleState
> {
export class Collapsible extends Component<CollapsibleProps, CollapsibleState> {
constructor(props: CollapsibleProps) {
super(props);
this.state = { collapsed: props.initiallyCollapsed };
@ -139,23 +136,20 @@ export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
return <span>{text}</span>;
}
export interface LoadingButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
export interface LoadingButtonProps
extends JSX.HTMLAttributes<HTMLButtonElement> {
isLoading: boolean;
}
export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.Element {
export function ProgressButton({
isLoading,
...rest
}: LoadingButtonProps): JSX.Element {
return (
<button
class="pure-button pure-button-primary"
type="button"
{...rest}
>
<button class="pure-button pure-button-primary" type="button" {...rest}>
{isLoading ? (
<span>
<object
class="svg-icon svg-baseline"
data="/img/spinner-bars.svg"
/>
<object class="svg-icon svg-baseline" data="/img/spinner-bars.svg" />
</span>
) : null}{" "}
{rest.children}
@ -163,17 +157,13 @@ export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.El
);
}
export function PageLink(
props: { pageName: string, children?: ComponentChildren },
): JSX.Element {
export function PageLink(props: {
pageName: string;
children?: ComponentChildren;
}): JSX.Element {
const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);
return (
<a
class="actionLink"
href={url}
target="_blank"
rel="noopener noreferrer"
>
<a class="actionLink" href={url} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
);

View File

@ -14,15 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ComponentChildren, FunctionalComponent, h as render } from 'preact';
import { ComponentChildren, FunctionalComponent, h as render } from "preact";
export function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
const r = (args: any) => render(Component, args)
r.args = props
return r
export function createExample<Props>(
Component: FunctionalComponent<Props>,
props: Partial<Props>,
) {
const r = (args: any) => render(Component, args);
r.args = props;
return r;
}
export function NullLink({ children }: { children?: ComponentChildren }) {
return render('a', { children, href: 'javascript:void(0);' })
return render("a", { children, href: "javascript:void(0);" });
}

View File

@ -15,179 +15,184 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { addDays } from 'date-fns';
import { BackupView as TestedComponent } from './BackupPage';
import { createExample } from '../test-utils';
import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { addDays } from "date-fns";
import { BackupView as TestedComponent } from "./BackupPage";
import { createExample } from "../test-utils";
export default {
title: 'wallet/backup/list',
title: "wallet/backup/list",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const LotOfProviders = createExample(TestedComponent, {
providers: [{
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": addDays(new Date(), 13).getTime()
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Pending,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.InsufficientBalance,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.TermsChanged,
newTerms: {
annualFee: 'USD:2',
storageLimitInMegabytes: 8,
supportedProtocolVersion: '2',
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
oldTerms: {
annualFee: 'USD:1',
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: '1',
supportedProtocolVersion: "0.0",
},
paidUntil: {
t_ms: 'never'
}
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: addDays(new Date(), 13).getTime(),
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}, {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}]
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
newTerms: {
annualFee: "USD:2",
storageLimitInMegabytes: 8,
supportedProtocolVersion: "2",
},
oldTerms: {
annualFee: "USD:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "1",
},
paidUntil: {
t_ms: "never",
},
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
{
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
terms: {
annualFee: "KUDOS:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
],
});
export const OneProvider = createExample(TestedComponent, {
providers: [{
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
providers: [
{
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
terms: {
annualFee: "ARS:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}]
],
});
export const Empty = createExample(TestedComponent, {
providers: []
providers: [],
});

View File

@ -14,15 +14,29 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentStatus } from "@gnu-taler/taler-wallet-core";
import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
import {
ProviderInfo,
ProviderPaymentStatus,
} from "@gnu-taler/taler-wallet-core";
import {
differenceInMonths,
formatDuration,
intervalToDuration,
} from "date-fns";
import { Fragment, JSX, VNode, h } from "preact";
import {
BoldLight, ButtonPrimary, ButtonSuccess, Centered,
CenteredText, CenteredBoldText, PopupBox, RowBorderGray,
SmallText, SmallLightText, WalletBox
BoldLight,
ButtonPrimary,
ButtonSuccess,
Centered,
CenteredText,
CenteredBoldText,
PopupBox,
RowBorderGray,
SmallText,
SmallLightText,
WalletBox,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "../NavigationBar";
@ -32,49 +46,68 @@ interface Props {
}
export function BackupPage({ onAddProvider }: Props): VNode {
const status = useBackupStatus()
const status = useBackupStatus();
if (!status) {
return <div>Loading...</div>
return <div>Loading...</div>;
}
return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
return (
<BackupView
providers={status.providers}
onAddProvider={onAddProvider}
onSyncAll={status.sync}
/>
);
}
export interface ViewProps {
providers: ProviderInfo[],
providers: ProviderInfo[];
onAddProvider: () => void;
onSyncAll: () => Promise<void>;
}
export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
export function BackupView({
providers,
onAddProvider,
onSyncAll,
}: ViewProps): VNode {
return (
<WalletBox>
<section>
{providers.map((provider) => <BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
active={provider.active}
title={provider.name}
/>
{providers.map((provider) => (
<BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
active={provider.active}
title={provider.name}
/>
))}
{!providers.length && (
<Centered style={{ marginTop: 100 }}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}>
<i18n.Translate>Add provider</i18n.Translate>
</ButtonSuccess>
</Centered>
)}
{!providers.length && <Centered style={{ marginTop: 100 }}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
</Centered>}
</section>
{!!providers.length && <footer>
<div />
<div>
<ButtonPrimary onClick={onSyncAll}>{
providers.length > 1 ?
<i18n.Translate>Sync all backups</i18n.Translate> :
<i18n.Translate>Sync now</i18n.Translate>
}</ButtonPrimary>
<ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
</div>
</footer>}
{!!providers.length && (
<footer>
<div />
<div>
<ButtonPrimary onClick={onSyncAll}>
{providers.length > 1 ? (
<i18n.Translate>Sync all backups</i18n.Translate>
) : (
<i18n.Translate>Sync now</i18n.Translate>
)}
</ButtonPrimary>
<ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
</div>
</footer>
)}
</WalletBox>
)
);
}
interface TransactionLayoutProps {
@ -92,55 +125,73 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
timeStyle: "short",
} as any);
return (
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
<a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
<a
href={Pages.provider_detail.replace(
":pid",
encodeURIComponent(props.id),
)}
>
<span>{props.title}</span>
</a>
{dateStr && <SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>}
{!dateStr && <SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>}
{dateStr && (
<SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>
)}
{!dateStr && (
<SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>
)}
</div>
<div>
{props.status?.type === 'paid' ?
<ExpirationText until={props.status.paidUntil} /> :
{props.status?.type === "paid" ? (
<ExpirationText until={props.status.paidUntil} />
) : (
<div>{props.status.type}</div>
}
)}
</div>
</RowBorderGray>
);
}
function ExpirationText({ until }: { until: Timestamp }) {
return <Fragment>
<CenteredText> Expires in </CenteredText>
<CenteredBoldText {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredBoldText>
</Fragment>
return (
<Fragment>
<CenteredText> Expires in </CenteredText>
<CenteredBoldText {...{ color: colorByTimeToExpire(until) }}>
{" "}
{daysUntil(until)}{" "}
</CenteredBoldText>
</Fragment>
);
}
function colorByTimeToExpire(d: Timestamp) {
if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
const months = differenceInMonths(d.t_ms, new Date())
return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
if (d.t_ms === "never") return "rgb(28, 184, 65)";
const months = differenceInMonths(d.t_ms, new Date());
return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)";
}
function daysUntil(d: Timestamp) {
if (d.t_ms === 'never') return undefined
if (d.t_ms === "never") return undefined;
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
})
});
const str = formatDuration(duration, {
delimiter: ', ',
delimiter: ", ",
format: [
duration?.years ? 'years' : (
duration?.months ? 'months' : (
duration?.days ? 'days' : (
duration.hours ? 'hours' : 'minutes'
)
)
)
]
})
return `${str}`
}
duration?.years
? "years"
: duration?.months
? "months"
: duration?.days
? "days"
: duration.hours
? "hours"
: "minutes",
],
});
return `${str}`;
}

View File

@ -15,28 +15,25 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, NullLink } from '../test-utils';
import { BalanceView as TestedComponent } from './BalancePage';
import { createExample, NullLink } from "../test-utils";
import { BalanceView as TestedComponent } from "./BalancePage";
export default {
title: 'wallet/balance',
title: "wallet/balance",
component: TestedComponent,
argTypes: {
}
argTypes: {},
};
export const NotYetLoaded = createExample(TestedComponent, {
});
export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
message: 'Network error'
message: "Network error",
},
Linker: NullLink,
});
@ -45,7 +42,7 @@ export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: []
balances: [],
},
},
Linker: NullLink,
@ -55,13 +52,15 @@ export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:10.5',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:10.5",
hasPendingTransactions: false,
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -71,13 +70,15 @@ export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2.23',
hasPendingTransactions: false,
pendingIncoming: 'USD:5.11',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:2.23",
hasPendingTransactions: false,
pendingIncoming: "USD:5.11",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,
@ -87,19 +88,22 @@ export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
balances: [{
available: 'USD:2',
hasPendingTransactions: false,
pendingIncoming: 'USD:5',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'EUR:4',
hasPendingTransactions: false,
pendingIncoming: 'EUR:5',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
}]
balances: [
{
available: "USD:2",
hasPendingTransactions: false,
pendingIncoming: "USD:5",
pendingOutgoing: "USD:0",
requiresUserInput: false,
},
{
available: "EUR:4",
hasPendingTransactions: false,
pendingIncoming: "EUR:5",
pendingOutgoing: "EUR:0",
requiresUserInput: false,
},
],
},
},
Linker: NullLink,

View File

@ -15,19 +15,30 @@
*/
import {
amountFractionalBase, Amounts,
Balance, BalancesResponse,
i18n
amountFractionalBase,
Amounts,
Balance,
BalancesResponse,
i18n,
} from "@gnu-taler/taler-util";
import { JSX } from "preact";
import { JSX, h } from "preact";
import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
const balance = useBalances()
return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
export function BalancePage({
goToWalletManualWithdraw,
}: {
goToWalletManualWithdraw: () => void;
}) {
const balance = useBalances();
return (
<BalanceView
balance={balance}
Linker={PageLink}
goToWalletManualWithdraw={goToWalletManualWithdraw}
/>
);
}
export interface BalanceViewProps {
@ -36,9 +47,13 @@ export interface BalanceViewProps {
goToWalletManualWithdraw: () => void;
}
export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
}: BalanceViewProps) {
if (!balance) {
return <span />
return <span />;
}
if (balance.hasError) {
@ -50,19 +65,24 @@ export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: Balan
diagnostics.
</p>
</div>
)
);
}
if (balance.response.balances.length === 0) {
return (
<p><i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
</i18n.Translate></p>
)
<p>
<i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
</i18n.Translate>
</p>
);
}
return <ShowBalances wallet={balance.response}
onWithdraw={goToWalletManualWithdraw}
/>
return (
<ShowBalances
wallet={balance.response}
onWithdraw={goToWalletManualWithdraw}
/>
);
}
function formatPending(entry: Balance): JSX.Element {
@ -75,13 +95,15 @@ function formatPending(entry: Balance): JSX.Element {
if (!Amounts.isZero(pendingIncoming)) {
incoming = (
<span><i18n.Translate>
<span style={{ color: "darkgreen" }}>
{"+"}
{renderAmount(entry.pendingIncoming)}
</span>{" "}
incoming
</i18n.Translate></span>
<span>
<i18n.Translate>
<span style={{ color: "darkgreen" }}>
{"+"}
{renderAmount(entry.pendingIncoming)}
</span>{" "}
incoming
</i18n.Translate>
</span>
);
}
@ -100,27 +122,36 @@ function formatPending(entry: Balance): JSX.Element {
);
}
function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) {
return <WalletBox>
<section>
<Centered>{wallet.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
const v = av.value + av.fraction / amountFractionalBase;
return (
<p key={av.currency}>
<span>
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
<span>{av.currency}</span>
</span>
{formatPending(entry)}
</p>
);
})}</Centered>
</section>
<footer>
<div />
<ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary>
</footer>
</WalletBox>
function ShowBalances({
wallet,
onWithdraw,
}: {
wallet: BalancesResponse;
onWithdraw: () => void;
}) {
return (
<WalletBox>
<section>
<Centered>
{wallet.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
const v = av.value + av.fraction / amountFractionalBase;
return (
<p key={av.currency}>
<span>
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
<span>{av.currency}</span>
</span>
{formatPending(entry)}
</p>
);
})}
</Centered>
</section>
<footer>
<div />
<ButtonPrimary onClick={onWithdraw}>Withdraw</ButtonPrimary>
</footer>
</WalletBox>
);
}

View File

@ -15,42 +15,39 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { CreateManualWithdraw as TestedComponent } from './CreateManualWithdraw';
import { createExample } from "../test-utils";
import { CreateManualWithdraw as TestedComponent } from "./CreateManualWithdraw";
export default {
title: 'wallet/manual withdraw/creation',
title: "wallet/manual withdraw/creation",
component: TestedComponent,
argTypes: {
}
argTypes: {},
};
export const InitialState = createExample(TestedComponent, {
});
export const InitialState = createExample(TestedComponent, {});
export const WithExchangeFilled = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
currency: "COL",
initialExchange: "http://exchange.taler:8081",
});
export const WithExchangeAndAmountFilled = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
initialAmount: '10'
currency: "COL",
initialExchange: "http://exchange.taler:8081",
initialAmount: "10",
});
export const WithExchangeError = createExample(TestedComponent, {
initialExchange: 'http://exchange.tal',
error: 'The exchange url seems invalid'
initialExchange: "http://exchange.tal",
error: "The exchange url seems invalid",
});
export const WithAmountError = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
initialAmount: 'e'
currency: "COL",
initialExchange: "http://exchange.taler:8081",
initialAmount: "e",
});

View File

@ -1,8 +1,35 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { VNode } from "preact";
import { VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage";
import { ButtonPrimary, Input, InputWithLabel, LightText, WalletBox } from "../components/styled";
import {
ButtonPrimary,
Input,
InputWithLabel,
LightText,
WalletBox,
} from "../components/styled";
export interface Props {
error: string | undefined;
@ -13,44 +40,73 @@ export interface Props {
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
}
export function CreateManualWithdraw({ onExchangeChange, initialExchange, initialAmount, error, currency, onCreate }: Props): VNode {
export function CreateManualWithdraw({
onExchangeChange,
initialExchange,
initialAmount,
error,
currency,
onCreate,
}: Props): VNode {
const [exchange, setExchange] = useState(initialExchange || "");
const [amount, setAmount] = useState(initialAmount || "");
const parsedAmount = Amounts.parse(`${currency}:${amount}`)
const parsedAmount = Amounts.parse(`${currency}:${amount}`);
let timeout = useRef<number | undefined>(undefined);
useEffect(() => {
if (timeout) window.clearTimeout(timeout.current)
if (timeout) window.clearTimeout(timeout.current);
timeout.current = window.setTimeout(async () => {
onExchangeChange(exchange)
onExchangeChange(exchange);
}, 1000);
}, [exchange])
}, [exchange]);
return (
<WalletBox>
<section>
<ErrorMessage title={error && "Can't create the reserve"} description={error} />
<ErrorMessage
title={error && "Can't create the reserve"}
description={error}
/>
<h2>Manual Withdrawal</h2>
<LightText>Choose a exchange to create a reserve and then fill the reserve to withdraw the coins</LightText>
<LightText>
Choose a exchange to create a reserve and then fill the reserve to
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
type="text"
placeholder="https://"
value={exchange}
onChange={(e) => setExchange(e.currentTarget.value)}
/>
<small>http://exchange.taler:8081</small>
</Input>
{currency && <InputWithLabel invalid={!!amount && !parsedAmount}>
<label>Amount</label>
<div>
<div>{currency}</div>
<input type="number" style={{ paddingLeft: `${currency.length}em` }} value={amount} onChange={e => setAmount(e.currentTarget.value)} />
</div>
</InputWithLabel>}
{currency && (
<InputWithLabel invalid={!!amount && !parsedAmount}>
<label>Amount</label>
<div>
<div>{currency}</div>
<input
type="number"
style={{ paddingLeft: `${currency.length}em` }}
value={amount}
onChange={(e) => setAmount(e.currentTarget.value)}
/>
</div>
</InputWithLabel>
)}
</p>
</section>
<footer>
<div />
<ButtonPrimary disabled={!parsedAmount || !exchange} onClick={() => onCreate(exchange, parsedAmount!)}>Create</ButtonPrimary>
<ButtonPrimary
disabled={!parsedAmount || !exchange}
onClick={() => onCreate(exchange, parsedAmount!)}
>
Create
</ButtonPrimary>
</footer>
</WalletBox>
);

View File

@ -15,133 +15,146 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
PaymentStatus,
TransactionCommon, TransactionDeposit, TransactionPayment,
TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalType
} from '@gnu-taler/taler-util';
import { HistoryView as TestedComponent } from './History';
import { createExample } from '../test-utils';
WithdrawalType,
} from "@gnu-taler/taler-util";
import { HistoryView as TestedComponent } from "./History";
import { createExample } from "../test-utils";
export default {
title: 'wallet/history/list',
title: "wallet/history/list",
component: TestedComponent,
};
let count = 0
const commonTransaction = () => ({
amountRaw: 'USD:10',
amountEffective: 'USD:9',
pending: false,
timestamp: {
t_ms: new Date().getTime() - (count++ * 1000 * 60 * 60 * 7)
},
transactionId: '12',
} as TransactionCommon)
let count = 0;
const commonTransaction = () =>
({
amountRaw: "USD:10",
amountEffective: "USD:9",
pending: false,
timestamp: {
t_ms: new Date().getTime() - count++ * 1000 * 60 * 60 * 7,
},
transactionId: "12",
} as TransactionCommon);
const exampleData = {
withdraw: {
...commonTransaction(),
type: TransactionType.Withdrawal,
exchangeBaseUrl: 'http://exchange.demo.taler.net',
exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
confirmed: false,
exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
}
},
} as TransactionWithdrawal,
payment: {
...commonTransaction(),
amountEffective: 'USD:11',
amountEffective: "USD:11",
type: TransactionType.Payment,
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'Blog',
name: "Blog",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: 'the summary',
fulfillmentMessage: '',
summary: "the summary",
fulfillmentMessage: "",
},
proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
deposit: {
...commonTransaction(),
type: TransactionType.Deposit,
depositGroupId: '#groupId',
targetPaytoUri: 'payto://x-taler-bank/bank/account',
depositGroupId: "#groupId",
targetPaytoUri: "payto://x-taler-bank/bank/account",
} as TransactionDeposit,
refresh: {
...commonTransaction(),
type: TransactionType.Refresh,
exchangeBaseUrl: 'http://exchange.taler',
exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction(),
type: TransactionType.Tip,
merchantBaseUrl: 'http://ads.merchant.taler.net/',
merchantBaseUrl: "http://ads.merchant.taler.net/",
} as TransactionTip,
refund: {
...commonTransaction(),
type: TransactionType.Refund,
refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'the merchant',
name: "the merchant",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: 'the summary',
fulfillmentMessage: '',
summary: "the summary",
fulfillmentMessage: "",
},
} as TransactionRefund,
}
};
export const Empty = createExample(TestedComponent, {
list: [],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
balances: [{
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const OnePending = createExample(TestedComponent, {
list: [{
...exampleData.withdraw,
pending: true
}],
balances: [{
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
list: [
{
...exampleData.withdraw,
pending: true,
},
],
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const Several = createExample(TestedComponent, {
@ -154,20 +167,23 @@ export const Several = createExample(TestedComponent, {
...exampleData.payment,
info: {
...exampleData.payment.info,
summary: 'this is a long summary that may be cropped because its too long',
summary:
"this is a long summary that may be cropped because its too long",
},
},
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
@ -181,18 +197,20 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
exampleData.tip,
exampleData.deposit,
],
balances: [{
available: 'TESTKUDOS:10',
pendingIncoming: 'TESTKUDOS:0',
pendingOutgoing: 'TESTKUDOS:0',
hasPendingTransactions: false,
requiresUserInput: false,
}, {
available: 'USD:10',
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
hasPendingTransactions: false,
requiresUserInput: false,
}]
balances: [
{
available: "TESTKUDOS:10",
pendingIncoming: "TESTKUDOS:0",
pendingOutgoing: "TESTKUDOS:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});

View File

@ -14,7 +14,12 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountString, Balance, Transaction, TransactionsResponse } from "@gnu-taler/taler-util";
import {
AmountString,
Balance,
Transaction,
TransactionsResponse,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { Fragment, h, JSX } from "preact";
import { useEffect, useState } from "preact/hooks";
@ -23,13 +28,14 @@ import { TransactionItem } from "../components/TransactionItem";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
export function HistoryPage(props: any): JSX.Element {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
const balance = useBalances()
const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
const balance = useBalances();
const balanceWithoutError = balance?.hasError
? []
: balance?.response.balances || [];
useEffect(() => {
const fetchData = async (): Promise<void> => {
@ -43,45 +49,74 @@ export function HistoryPage(props: any): JSX.Element {
return <div>Loading ...</div>;
}
return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
return (
<HistoryView
balances={balanceWithoutError}
list={[...transactions.transactions].reverse()}
/>
);
}
function amountToString(c: AmountString) {
const idx = c.indexOf(':')
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`
const idx = c.indexOf(":");
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
}
export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) {
export function HistoryView({
list,
balances,
}: {
list: Transaction[];
balances: Balance[];
}) {
const byDate = list.reduce(function (rv, x) {
const theDate = x.timestamp.t_ms === "never" ? "never" : format(x.timestamp.t_ms, 'dd MMMM yyyy');
const theDate =
x.timestamp.t_ms === "never"
? "never"
: format(x.timestamp.t_ms, "dd MMMM yyyy");
(rv[theDate] = rv[theDate] || []).push(x);
return rv;
}, {} as { [x: string]: Transaction[] });
const multiCurrency = balances.length > 1
const multiCurrency = balances.length > 1;
return <WalletBox noPadding>
{balances.length > 0 && <header>
{balances.length === 1 && <div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>}
{balances.length > 1 && <div class="title">
Balance: <ul style={{ margin: 0 }}>
{balances.map(b => <li>{b.available}</li>)}
</ul>
</div>}
</header>}
<section>
{Object.keys(byDate).map((d,i) => {
return <Fragment key={i}>
<DateSeparator>{d}</DateSeparator>
{byDate[d].map((tx, i) => (
<TransactionItem key={i} tx={tx} multiCurrency={multiCurrency}/>
))}
</Fragment>
})}
</section>
</WalletBox>
return (
<WalletBox noPadding>
{balances.length > 0 && (
<header>
{balances.length === 1 && (
<div class="title">
Balance: <span>{amountToString(balances[0].available)}</span>
</div>
)}
{balances.length > 1 && (
<div class="title">
Balance:{" "}
<ul style={{ margin: 0 }}>
{balances.map((b) => (
<li>{b.available}</li>
))}
</ul>
</div>
)}
</header>
)}
<section>
{Object.keys(byDate).map((d, i) => {
return (
<Fragment key={i}>
<DateSeparator>{d}</DateSeparator>
{byDate[d].map((tx, i) => (
<TransactionItem
key={i}
tx={tx}
multiCurrency={multiCurrency}
/>
))}
</Fragment>
);
})}
</section>
</WalletBox>
);
}

View File

@ -14,68 +14,84 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { CreateManualWithdraw } from "./CreateManualWithdraw";
import * as wxApi from '../wxApi'
import { AcceptManualWithdrawalResult, AmountJson, Amounts } from "@gnu-taler/taler-util";
import * as wxApi from "../wxApi";
import {
AcceptManualWithdrawalResult,
AmountJson,
Amounts,
} from "@gnu-taler/taler-util";
import { ReserveCreated } from "./ReserveCreated.js";
import { route } from 'preact-router';
import { route } from "preact-router";
import { Pages } from "../NavigationBar.js";
interface Props {
interface Props {}
}
export function ManualWithdrawPage({}: Props): VNode {
const [success, setSuccess] = useState<
AcceptManualWithdrawalResult | undefined
>(undefined);
const [currency, setCurrency] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
export function ManualWithdrawPage({ }: Props): VNode {
const [success, setSuccess] = useState<AcceptManualWithdrawalResult | undefined>(undefined)
const [currency, setCurrency] = useState<string | undefined>(undefined)
const [error, setError] = useState<string | undefined>(undefined)
async function onExchangeChange(exchange: string | undefined) {
if (!exchange) return
async function onExchangeChange(exchange: string | undefined): Promise<void> {
if (!exchange) return;
try {
const r = await fetch(`${exchange}/keys`)
const j = await r.json()
const r = await fetch(`${exchange}/keys`);
const j = await r.json();
if (j.currency) {
await wxApi.addExchange({
exchangeBaseUrl: `${exchange}/`,
forceUpdate: true
})
setCurrency(j.currency)
forceUpdate: true,
});
setCurrency(j.currency);
}
} catch (e) {
setError('The exchange url seems invalid')
setCurrency(undefined)
setError("The exchange url seems invalid");
setCurrency(undefined);
}
}
async function doCreate(exchangeBaseUrl: string, amount: AmountJson) {
async function doCreate(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<void> {
try {
const resp = await wxApi.acceptManualWithdrawal(exchangeBaseUrl, Amounts.stringify(amount))
setSuccess(resp)
const resp = await wxApi.acceptManualWithdrawal(
exchangeBaseUrl,
Amounts.stringify(amount),
);
setSuccess(resp);
} catch (e) {
if (e instanceof Error) {
setError(e.message)
setError(e.message);
} else {
setError('unexpected error')
setError("unexpected error");
}
setSuccess(undefined)
setSuccess(undefined);
}
}
if (success) {
return <ReserveCreated reservePub={success.reservePub} paytos={success.exchangePaytoUris} onBack={() => {
route(Pages.balance)
}}/>
return (
<ReserveCreated
reservePub={success.reservePub}
paytos={success.exchangePaytoUris}
onBack={() => {
route(Pages.balance);
}}
/>
);
}
return <CreateManualWithdraw
error={error} currency={currency}
onCreate={doCreate} onExchangeChange={onExchangeChange}
/>;
return (
<CreateManualWithdraw
error={error}
currency={currency}
onCreate={doCreate}
onExchangeChange={onExchangeChange}
/>
);
}

View File

@ -15,38 +15,37 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { ConfirmProviderView as TestedComponent } from './ProviderAddPage';
import { createExample } from "../test-utils";
import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage";
export default {
title: 'wallet/backup/confirm',
title: "wallet/backup/confirm",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const DemoService = createExample(TestedComponent, {
url: 'https://sync.demo.taler.net/',
url: "https://sync.demo.taler.net/",
provider: {
annual_fee: 'KUDOS:0.1',
storage_limit_in_megabytes: 20,
supported_protocol_version: '1'
}
annual_fee: "KUDOS:0.1",
storage_limit_in_megabytes: 20,
supported_protocol_version: "1",
},
});
export const FreeService = createExample(TestedComponent, {
url: 'https://sync.taler:9667/',
url: "https://sync.taler:9667/",
provider: {
annual_fee: 'ARS:0',
storage_limit_in_megabytes: 20,
supported_protocol_version: '1'
}
annual_fee: "ARS:0",
storage_limit_in_megabytes: 20,
supported_protocol_version: "1",
},
});

View File

@ -15,39 +15,37 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { SetUrlView as TestedComponent } from './ProviderAddPage';
import { createExample } from "../test-utils";
import { SetUrlView as TestedComponent } from "./ProviderAddPage";
export default {
title: 'wallet/backup/add',
title: "wallet/backup/add",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const Initial = createExample(TestedComponent, {
});
export const Initial = createExample(TestedComponent, {});
export const WithValue = createExample(TestedComponent, {
initialValue: 'sync.demo.taler.net'
});
initialValue: "sync.demo.taler.net",
});
export const WithConnectionError = createExample(TestedComponent, {
withError: 'Network error'
});
withError: "Network error",
});
export const WithClientError = createExample(TestedComponent, {
withError: 'URL may not be right: (404) Not Found'
});
withError: "URL may not be right: (404) Not Found",
});
export const WithServerError = createExample(TestedComponent, {
withError: 'Try another server: (500) Internal Server Error'
});
withError: "Try another server: (500) Internal Server Error",
});

View File

@ -15,224 +15,221 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { createExample } from '../test-utils';
import { ProviderView as TestedComponent } from './ProviderDetailPage';
import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { createExample } from "../test-utils";
import { ProviderView as TestedComponent } from "./ProviderDetailPage";
export default {
title: 'wallet/backup/details',
title: "wallet/backup/details",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
export const Active = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveErrorSync = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
lastAttemptedBackupTimestamp: {
"t_ms": 1625063925078
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
lastError: {
code: 2002,
details: 'details',
hint: 'error hint from the server',
message: 'message'
details: "details",
hint: "error hint from the server",
message: "message",
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
backupProblem: {
type: 'backup-unreadable'
type: "backup-unreadable",
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveBackupProblemDevice = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.taler:9967/",
lastSuccessfulBackupTimestamp: {
t_ms: 1625063925078,
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
paymentProposalIds: [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
paymentStatus: {
type: ProviderPaymentType.Paid,
paidUntil: {
t_ms: 1656599921000,
},
},
backupProblem: {
type: 'backup-conflicting-device',
myDeviceId: 'my-device-id',
otherDeviceId: 'other-device-id',
type: "backup-conflicting-device",
myDeviceId: "my-device-id",
otherDeviceId: "other-device-id",
backupTimestamp: {
"t_ms": 1656599921000
}
t_ms: 1656599921000,
},
},
"terms": {
"annualFee": "EUR:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactiveUnpaid = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Unpaid,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactiveInsufficientBalance = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.InsufficientBalance,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.InsufficientBalance,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const InactivePending = createExample(TestedComponent, {
info: {
"active": false,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Pending,
active: false,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.Pending,
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});
export const ActiveTermsChanged = createExample(TestedComponent, {
info: {
"active": true,
name:'sync.demo',
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.TermsChanged,
active: true,
name: "sync.demo",
syncProviderBaseUrl: "http://sync.demo.taler.net/",
paymentProposalIds: [],
paymentStatus: {
type: ProviderPaymentType.TermsChanged,
paidUntil: {
t_ms: 1656599921000
t_ms: 1656599921000,
},
newTerms: {
"annualFee": "EUR:10",
"storageLimitInMegabytes": 8,
"supportedProtocolVersion": "0.0"
annualFee: "EUR:10",
storageLimitInMegabytes: 8,
supportedProtocolVersion: "0.0",
},
oldTerms: {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
terms: {
annualFee: "EUR:0.1",
storageLimitInMegabytes: 16,
supportedProtocolVersion: "0.0",
},
},
});

View File

@ -14,13 +14,23 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import {
ProviderInfo,
ProviderPaymentStatus,
ProviderPaymentType,
} from "@gnu-taler/taler-wallet-core";
import { format, formatDuration, intervalToDuration } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, WalletBox, SmallLightText } from "../components/styled";
import {
Button,
ButtonDestructive,
ButtonPrimary,
PaymentStatus,
WalletBox,
SmallLightText,
} from "../components/styled";
import { useProviderStatus } from "../hooks/useProviderStatus";
interface Props {
@ -29,20 +39,29 @@ interface Props {
}
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
const status = useProviderStatus(pid)
const status = useProviderStatus(pid);
if (!status) {
return <div><i18n.Translate>Loading...</i18n.Translate></div>
return (
<div>
<i18n.Translate>Loading...</i18n.Translate>
</div>
);
}
if (!status.info) {
onBack()
return <div />
onBack();
return <div />;
}
return <ProviderView info={status.info}
onSync={status.sync}
onDelete={() => status.remove().then(onBack)}
onBack={onBack}
onExtend={() => { null }}
/>;
return (
<ProviderView
info={status.info}
onSync={status.sync}
onDelete={() => status.remove().then(onBack)}
onBack={onBack}
onExtend={() => {
null;
}}
/>
);
}
export interface ViewProps {
@ -53,124 +72,185 @@ export interface ViewProps {
onExtend: () => void;
}
export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode {
const lb = info?.lastSuccessfulBackupTimestamp
const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged
export function ProviderView({
info,
onDelete,
onSync,
onBack,
onExtend,
}: ViewProps): VNode {
const lb = info?.lastSuccessfulBackupTimestamp;
const isPaid =
info.paymentStatus.type === ProviderPaymentType.Paid ||
info.paymentStatus.type === ProviderPaymentType.TermsChanged;
return (
<WalletBox>
<Error info={info} />
<header>
<h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3>
<PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus>
<h3>
{info.name}{" "}
<SmallLightText>{info.syncProviderBaseUrl}</SmallLightText>
</h3>
<PaymentStatus color={isPaid ? "rgb(28, 184, 65)" : "rgb(202, 60, 60)"}>
{isPaid ? "Paid" : "Unpaid"}
</PaymentStatus>
</header>
<section>
<p><b>Last backup:</b> {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')} </p>
<ButtonPrimary onClick={onSync}><i18n.Translate>Back up</i18n.Translate></ButtonPrimary>
{info.terms && <Fragment>
<p><b>Provider fee:</b> {info.terms && info.terms.annualFee} per year</p>
</Fragment>
}
<p>
<b>Last backup:</b>{" "}
{lb == null || lb.t_ms == "never"
? "never"
: format(lb.t_ms, "dd MMM yyyy")}{" "}
</p>
<ButtonPrimary onClick={onSync}>
<i18n.Translate>Back up</i18n.Translate>
</ButtonPrimary>
{info.terms && (
<Fragment>
<p>
<b>Provider fee:</b> {info.terms && info.terms.annualFee} per year
</p>
</Fragment>
)}
<p>{descriptionByStatus(info.paymentStatus)}</p>
<ButtonPrimary disabled onClick={onExtend}><i18n.Translate>Extend</i18n.Translate></ButtonPrimary>
{info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
<p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p>
<table>
<thead>
<tr>
<td></td>
<td><i18n.Translate>old</i18n.Translate></td>
<td> -&gt;</td>
<td><i18n.Translate>new</i18n.Translate></td>
</tr>
</thead>
<tbody>
<tr>
<td><i18n.Translate>fee</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td><i18n.Translate>storage</i18n.Translate></td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>}
<ButtonPrimary disabled onClick={onExtend}>
<i18n.Translate>Extend</i18n.Translate>
</ButtonPrimary>
{info.paymentStatus.type === ProviderPaymentType.TermsChanged && (
<div>
<p>
<i18n.Translate>
terms has changed, extending the service will imply accepting
the new terms of service
</i18n.Translate>
</p>
<table>
<thead>
<tr>
<td></td>
<td>
<i18n.Translate>old</i18n.Translate>
</td>
<td> -&gt;</td>
<td>
<i18n.Translate>new</i18n.Translate>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<i18n.Translate>fee</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td>
<i18n.Translate>storage</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>
)}
</section>
<footer>
<Button onClick={onBack}><i18n.Translate> &lt; back</i18n.Translate></Button>
<Button onClick={onBack}>
<i18n.Translate> &lt; back</i18n.Translate>
</Button>
<div>
<ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
<ButtonDestructive onClick={onDelete}>
<i18n.Translate>remove provider</i18n.Translate>
</ButtonDestructive>
</div>
</footer>
</WalletBox>
)
);
}
function daysSince(d?: Timestamp) {
if (!d || d.t_ms === 'never') return 'never synced'
if (!d || d.t_ms === "never") return "never synced";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
})
});
const str = formatDuration(duration, {
delimiter: ', ',
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`
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 }) {
if (info.lastError) {
return <ErrorMessage title={info.lastError.hint} />
return <ErrorMessage title={info.lastError.hint} />;
}
if (info.backupProblem) {
switch (info.backupProblem.type) {
case "backup-conflicting-device":
return <ErrorMessage title={<Fragment>
<i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
</Fragment>} />
return (
<ErrorMessage
title={
<Fragment>
<i18n.Translate>
There is conflict with another backup from{" "}
<b>{info.backupProblem.otherDeviceId}</b>
</i18n.Translate>
</Fragment>
}
/>
);
case "backup-unreadable":
return <ErrorMessage title="Backup is not readable" />
return <ErrorMessage title="Backup is not readable" />;
default:
return <ErrorMessage title={<Fragment>
<i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate>
</Fragment>} />
return (
<ErrorMessage
title={
<Fragment>
<i18n.Translate>
Unknown backup problem: {JSON.stringify(info.backupProblem)}
</i18n.Translate>
</Fragment>
}
/>
);
}
}
return null
return null;
}
function colorByStatus(status: ProviderPaymentType) {
switch (status) {
case ProviderPaymentType.InsufficientBalance:
return 'rgb(223, 117, 20)'
return "rgb(223, 117, 20)";
case ProviderPaymentType.Unpaid:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
case ProviderPaymentType.Paid:
return 'rgb(28, 184, 65)'
return "rgb(28, 184, 65)";
case ProviderPaymentType.Pending:
return 'gray'
return "gray";
case ProviderPaymentType.InsufficientBalance:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
case ProviderPaymentType.TermsChanged:
return 'rgb(202, 60, 60)'
return "rgb(202, 60, 60)";
}
}
@ -180,16 +260,19 @@ function descriptionByStatus(status: ProviderPaymentStatus) {
// return i18n.str`not paid yet`
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
if (status.paidUntil.t_ms === 'never') {
return i18n.str`service paid`
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 (
<Fragment>
<b>Backup valid until:</b>{" "}
{format(status.paidUntil.t_ms, "dd MMM yyyy")}
</Fragment>
);
}
case ProviderPaymentType.Unpaid:
case ProviderPaymentType.InsufficientBalance:
case ProviderPaymentType.Pending:
return ''
return "";
}
}

View File

@ -15,26 +15,23 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { ReserveCreated as TestedComponent } from './ReserveCreated';
import { createExample } from "../test-utils";
import { ReserveCreated as TestedComponent } from "./ReserveCreated";
export default {
title: 'wallet/manual withdraw/reserve created',
title: "wallet/manual withdraw/reserve created",
component: TestedComponent,
argTypes: {
}
argTypes: {},
};
export const InitialState = createExample(TestedComponent, {
reservePub: 'ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ',
reservePub: "ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ",
paytos: [
'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',
]
"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",
],
});

View File

@ -1,8 +1,7 @@
import { Fragment, VNode } from "preact";
import { h, Fragment, VNode } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../components/QR";
import { ButtonBox, FontIcon, WalletBox } from "../components/styled";
export interface Props {
reservePub: string;
paytos: string[];
@ -10,30 +9,57 @@ export interface Props {
}
export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode {
const [opened, setOpened] = useState(-1)
const [opened, setOpened] = useState(-1);
return (
<WalletBox>
<section>
<h2>Reserve created!</h2>
<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.</p>
<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.
</p>
</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>
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>
</section>
<footer>
<ButtonBox onClick={onBack}><FontIcon>&#x2190;</FontIcon></ButtonBox>
<ButtonBox onClick={onBack}>
<FontIcon>&#x2190;</FontIcon>
</ButtonBox>
<div />
</footer>
</WalletBox>

View File

@ -15,39 +15,41 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { SettingsView as TestedComponent } from './Settings';
import { createExample } from "../test-utils";
import { SettingsView as TestedComponent } from "./Settings";
export default {
title: 'wallet/settings',
title: "wallet/settings",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
}
},
};
export const AllOff = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
deviceName: "this-is-the-device-name",
setDeviceName: () => Promise.resolve(),
});
export const OneChecked = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
});
export const WithOneExchange = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
knownExchanges: [{
currency: 'USD',
exchangeBaseUrl: 'http://exchange.taler',
paytoUris: ['payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator']
}]
knownExchanges: [
{
currency: "USD",
exchangeBaseUrl: "http://exchange.taler",
paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
},
],
});

View File

@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
import { VNode, h, Fragment } from "preact";
import { Checkbox } from "../components/Checkbox";
@ -30,18 +29,28 @@ import * as wxApi from "../wxApi";
export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const { devMode, toggleDevMode } = useDevContext()
const { name, update } = useBackupDeviceName()
const [lang, changeLang] = useLang()
const { devMode, toggleDevMode } = useDevContext();
const { name, update } = useBackupDeviceName();
const [lang, changeLang] = useLang();
const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
return <SettingsView
lang={lang} changeLang={changeLang}
knownExchanges={!exchangesHook || exchangesHook.hasError ? [] : exchangesHook.response.exchanges}
deviceName={name} setDeviceName={update}
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
developerMode={devMode} toggleDeveloperMode={toggleDevMode}
/>;
return (
<SettingsView
lang={lang}
changeLang={changeLang}
knownExchanges={
!exchangesHook || exchangesHook.hasError
? []
: exchangesHook.response.exchanges
}
deviceName={name}
setDeviceName={update}
permissionsEnabled={permissionsEnabled}
togglePermissions={togglePermissions}
developerMode={devMode}
toggleDeveloperMode={toggleDevMode}
/>
);
}
export interface ViewProps {
@ -56,52 +65,72 @@ export interface ViewProps {
knownExchanges: Array<ExchangeListItem>;
}
import { strings as messages } from '../i18n/strings'
import { strings as messages } from "../i18n/strings";
type LangsNames = {
[P in keyof typeof messages]: string
}
[P in keyof typeof messages]: string;
};
const names: LangsNames = {
es: 'Español [es]',
en: 'English [en]',
fr: 'Français [fr]',
de: 'Deutsch [de]',
sv: 'Svenska [sv]',
it: 'Italiano [it]',
}
es: "Español [es]",
en: "English [en]",
fr: "Français [fr]",
de: "Deutsch [de]",
sv: "Svenska [sv]",
it: "Italiano [it]",
};
export function SettingsView({ knownExchanges, lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
export function SettingsView({
knownExchanges,
lang,
changeLang,
deviceName,
setDeviceName,
permissionsEnabled,
togglePermissions,
developerMode,
toggleDeveloperMode,
}: ViewProps): VNode {
return (
<WalletBox>
<section>
<h2><i18n.Translate>Known exchanges</i18n.Translate></h2>
{!knownExchanges || !knownExchanges.length ? <div>
No exchange yet!
</div> :
<h2>
<i18n.Translate>Known exchanges</i18n.Translate>
</h2>
{!knownExchanges || !knownExchanges.length ? (
<div>No exchange yet!</div>
) : (
<table>
{knownExchanges.map(e => <tr>
<td>{e.currency}</td>
<td><a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a></td>
</tr>)}
{knownExchanges.map((e) => (
<tr>
<td>{e.currency}</td>
<td>
<a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
</td>
</tr>
))}
</table>
}
<h2><i18n.Translate>Permissions</i18n.Translate></h2>
<Checkbox label="Automatically open wallet based on page content"
)}
<h2>
<i18n.Translate>Permissions</i18n.Translate>
</h2>
<Checkbox
label="Automatically open wallet based on page content"
name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
enabled={permissionsEnabled} onToggle={togglePermissions}
enabled={permissionsEnabled}
onToggle={togglePermissions}
/>
<h2>Config</h2>
<Checkbox label="Developer mode"
<Checkbox
label="Developer mode"
name="devMode"
description="(More options and information useful for debugging)"
enabled={developerMode} onToggle={toggleDeveloperMode}
enabled={developerMode}
onToggle={toggleDeveloperMode}
/>
</section>
</WalletBox>
)
);
}

View File

@ -15,110 +15,116 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
PaymentStatus,
TransactionCommon, TransactionDeposit, TransactionPayment,
TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionRefresh,
TransactionRefund,
TransactionTip,
TransactionType,
TransactionWithdrawal,
WithdrawalType
} from '@gnu-taler/taler-util';
import { createExample } from '../test-utils';
import { TransactionView as TestedComponent } from './Transaction';
WithdrawalType,
} from "@gnu-taler/taler-util";
import { createExample } from "../test-utils";
import { TransactionView as TestedComponent } from "./Transaction";
export default {
title: 'wallet/history/details',
title: "wallet/history/details",
component: TestedComponent,
argTypes: {
onRetry: { action: 'onRetry' },
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
}
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
};
const commonTransaction = {
amountRaw: 'KUDOS:11',
amountEffective: 'KUDOS:9.2',
amountRaw: "KUDOS:11",
amountEffective: "KUDOS:9.2",
pending: false,
timestamp: {
t_ms: new Date().getTime()
t_ms: new Date().getTime(),
},
transactionId: '12',
} as TransactionCommon
transactionId: "12",
} as TransactionCommon;
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
exchangeBaseUrl: 'http://exchange.taler',
exchangeBaseUrl: "http://exchange.taler",
withdrawalDetails: {
confirmed: false,
exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
}
},
} as TransactionWithdrawal,
payment: {
...commonTransaction,
amountEffective: 'KUDOS:11',
amountEffective: "KUDOS:11",
type: TransactionType.Payment,
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'the merchant',
name: "the merchant",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: '',
fulfillmentMessage: "",
},
proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
deposit: {
...commonTransaction,
type: TransactionType.Deposit,
depositGroupId: '#groupId',
targetPaytoUri: 'payto://x-taler-bank/bank/account',
depositGroupId: "#groupId",
targetPaytoUri: "payto://x-taler-bank/bank/account",
} as TransactionDeposit,
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
exchangeBaseUrl: 'http://exchange.taler',
exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
merchantBaseUrl: 'http://merchant.taler',
merchantBaseUrl: "http://merchant.taler",
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
refundedTransactionId:
"payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
contractTermsHash: 'ASDZXCASD',
contractTermsHash: "ASDZXCASD",
merchant: {
name: 'the merchant',
name: "the merchant",
},
orderId: '2021.167-03NPY6MCYMVGT',
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: 'the summary',
fulfillmentMessage: '',
summary: "the summary",
fulfillmentMessage: "",
},
} as TransactionRefund,
}
};
const transactionError = {
code: 2000,
details: "details",
hint: "this is a hint for the error",
message: 'message'
}
message: "message",
};
export const Withdraw = createExample(TestedComponent, {
transaction: exampleData.withdraw
transaction: exampleData.withdraw,
});
export const WithdrawError = createExample(TestedComponent, {
@ -132,24 +138,22 @@ export const WithdrawPending = createExample(TestedComponent, {
transaction: { ...exampleData.withdraw, pending: true },
});
export const Payment = createExample(TestedComponent, {
transaction: exampleData.payment
transaction: exampleData.payment,
});
export const PaymentError = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
error: transactionError
error: transactionError,
},
});
export const PaymentWithoutFee = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: 'KUDOS:11',
}
amountRaw: "KUDOS:11",
},
});
export const PaymentPending = createExample(TestedComponent, {
@ -161,27 +165,33 @@ export const PaymentWithProducts = createExample(TestedComponent, {
...exampleData.payment,
info: {
...exampleData.payment.info,
summary: 'this order has 5 products',
products: [{
description: 't-shirt',
unit: 'shirts',
quantity: 1,
}, {
description: 't-shirt',
unit: 'shirts',
quantity: 1,
}, {
description: 'e-book',
}, {
description: 'beer',
unit: 'pint',
quantity: 15,
}, {
description: 'beer',
unit: 'pint',
quantity: 15,
}]
}
summary: "this order has 5 products",
products: [
{
description: "t-shirt",
unit: "shirts",
quantity: 1,
},
{
description: "t-shirt",
unit: "shirts",
quantity: 1,
},
{
description: "e-book",
},
{
description: "beer",
unit: "pint",
quantity: 15,
},
{
description: "beer",
unit: "pint",
quantity: 15,
},
],
},
} as TransactionPayment,
});
@ -190,75 +200,79 @@ export const PaymentWithLongSummary = createExample(TestedComponent, {
...exampleData.payment,
info: {
...exampleData.payment.info,
summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ',
products: [{
description: 'an xl sized t-shirt with some drawings on it, color pink',
unit: 'shirts',
quantity: 1,
}, {
description: 'beer',
unit: 'pint',
quantity: 15,
}]
}
summary:
"this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ",
products: [
{
description:
"an xl sized t-shirt with some drawings on it, color pink",
unit: "shirts",
quantity: 1,
},
{
description: "beer",
unit: "pint",
quantity: 15,
},
],
},
} as TransactionPayment,
});
export const Deposit = createExample(TestedComponent, {
transaction: exampleData.deposit
transaction: exampleData.deposit,
});
export const DepositError = createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
error: transactionError
error: transactionError,
},
});
export const DepositPending = createExample(TestedComponent, {
transaction: { ...exampleData.deposit, pending: true }
transaction: { ...exampleData.deposit, pending: true },
});
export const Refresh = createExample(TestedComponent, {
transaction: exampleData.refresh
transaction: exampleData.refresh,
});
export const RefreshError = createExample(TestedComponent, {
transaction: {
...exampleData.refresh,
error: transactionError
error: transactionError,
},
});
export const Tip = createExample(TestedComponent, {
transaction: exampleData.tip
transaction: exampleData.tip,
});
export const TipError = createExample(TestedComponent, {
transaction: {
...exampleData.tip,
error: transactionError
error: transactionError,
},
});
export const TipPending = createExample(TestedComponent, {
transaction: { ...exampleData.tip, pending: true }
transaction: { ...exampleData.tip, pending: true },
});
export const Refund = createExample(TestedComponent, {
transaction: exampleData.refund
transaction: exampleData.refund,
});
export const RefundError = createExample(TestedComponent, {
transaction: {
...exampleData.refund,
error: transactionError
error: transactionError,
},
});
export const RefundPending = createExample(TestedComponent, {
transaction: { ...exampleData.refund, pending: true }
transaction: { ...exampleData.refund, pending: true },
});
export const RefundWithProducts = createExample(TestedComponent, {
@ -266,11 +280,14 @@ export const RefundWithProducts = createExample(TestedComponent, {
...exampleData.refund,
info: {
...exampleData.refund.info,
products: [{
description: 't-shirt',
}, {
description: 'beer',
}]
}
products: [
{
description: "t-shirt",
},
{
description: "beer",
},
],
},
} as TransactionRefund,
});

View File

@ -14,27 +14,43 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";
import {
AmountLike,
Amounts,
i18n,
Transaction,
TransactionType,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { JSX, VNode } from "preact";
import { route } from 'preact-router';
import { JSX, VNode, h } 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, ButtonPrimary, FontIcon, ListOfProducts, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled";
import {
ButtonBox,
ButtonBoxDestructive,
ButtonPrimary,
FontIcon,
ListOfProducts,
RowBorderGray,
SmallLightText,
WalletBox,
WarningBox,
} from "../components/styled";
import { Pages } from "../NavigationBar";
import * as wxApi from "../wxApi";
export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
const [transaction, setTransaction] = useState<
Transaction | undefined
>(undefined);
export function TransactionPage({ tid }: { tid: string }): JSX.Element {
const [transaction, setTransaction] = useState<Transaction | undefined>(
undefined,
);
useEffect(() => {
const fetchData = async (): Promise<void> => {
const res = await wxApi.getTransactions();
const ts = res.transactions.filter(t => t.transactionId === tid);
const ts = res.transactions.filter((t) => t.transactionId === tid);
if (ts.length === 1) {
setTransaction(ts[0]);
} else {
@ -45,13 +61,22 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
}, [tid]);
if (!transaction) {
return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
return (
<div>
<i18n.Translate>Loading ...</i18n.Translate>
</div>
);
}
return <TransactionView
transaction={transaction}
onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))}
onBack={() => { route(Pages.history) }} />;
return (
<TransactionView
transaction={transaction}
onDelete={() => wxApi.deleteTransaction(tid).then((_) => history.go(-1))}
onRetry={() => wxApi.retryTransaction(tid).then((_) => history.go(-1))}
onBack={() => {
route(Pages.history);
}}
/>
);
}
export interface WalletTransactionProps {
@ -61,173 +86,310 @@ export interface WalletTransactionProps {
onBack: () => void;
}
export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
export function TransactionView({
transaction,
onDelete,
onRetry,
onBack,
}: WalletTransactionProps) {
function TransactionTemplate({ children }: { children: VNode[] }) {
return <WalletBox>
<section style={{ padding: 8, textAlign: 'center'}}>
<ErrorMessage title={transaction?.error?.hint} />
{transaction.pending && <WarningBox>This transaction is not completed</WarningBox>}
</section>
<section>
<div style={{ textAlign: 'center' }}>
{children}
</div>
</section>
<footer>
<ButtonBox onClick={onBack}><i18n.Translate> <FontIcon>&#x2190;</FontIcon> </i18n.Translate></ButtonBox>
<div>
{transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null}
<ButtonBoxDestructive onClick={onDelete}><i18n.Translate>&#x1F5D1;</i18n.Translate></ButtonBoxDestructive>
</div>
</footer>
</WalletBox>
return (
<WalletBox>
<section style={{ padding: 8, textAlign: "center" }}>
<ErrorMessage title={transaction?.error?.hint} />
{transaction.pending && (
<WarningBox>This transaction is not completed</WarningBox>
)}
</section>
<section>
<div style={{ textAlign: "center" }}>{children}</div>
</section>
<footer>
<ButtonBox onClick={onBack}>
<i18n.Translate>
{" "}
<FontIcon>&#x2190;</FontIcon>{" "}
</i18n.Translate>
</ButtonBox>
<div>
{transaction?.error ? (
<ButtonPrimary onClick={onRetry}>
<i18n.Translate>retry</i18n.Translate>
</ButtonPrimary>
) : null}
<ButtonBoxDestructive onClick={onDelete}>
<i18n.Translate>&#x1F5D1;</i18n.Translate>
</ButtonBoxDestructive>
</div>
</footer>
</WalletBox>
);
}
function amountToString(text: AmountLike) {
const aj = Amounts.jsonifyAmount(text)
const amount = Amounts.stringifyValue(aj)
return `${amount} ${aj.currency}`
const aj = Amounts.jsonifyAmount(text);
const amount = Amounts.stringifyValue(aj);
return `${amount} ${aj.currency}`;
}
if (transaction.type === TransactionType.Withdrawal) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
return <TransactionTemplate>
<h2>Withdrawal</h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part title="Total withdrawn" text={amountToString(transaction.amountEffective)} kind='positive' />
<Part title="Chosen amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part title="Exchange fee" text={amountToString(fee)} kind='negative' />
<Part title="Exchange" text={new URL(transaction.exchangeBaseUrl).hostname} kind='neutral' />
</TransactionTemplate>
).amount;
return (
<TransactionTemplate>
<h2>Withdrawal</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
title="Total withdrawn"
text={amountToString(transaction.amountEffective)}
kind="positive"
/>
<Part
title="Chosen amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part title="Exchange fee" text={amountToString(fee)} kind="negative" />
<Part
title="Exchange"
text={new URL(transaction.exchangeBaseUrl).hostname}
kind="neutral"
/>
</TransactionTemplate>
);
}
const showLargePic = () => {
}
const showLargePic = () => {};
if (transaction.type === TransactionType.Payment) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountEffective),
Amounts.parseOrThrow(transaction.amountRaw),
).amount
).amount;
return <TransactionTemplate>
<h2>Payment </h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part big title="Total paid" text={amountToString(transaction.amountEffective)} kind='negative' />
<Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
<Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
<Part title="Purchase" text={transaction.info.summary} kind='neutral' />
<Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
return (
<TransactionTemplate>
<h2>Payment </h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
big
title="Total paid"
text={amountToString(transaction.amountEffective)}
kind="negative"
/>
<Part
big
title="Purchase amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
<Part
title="Merchant"
text={transaction.info.merchant.name}
kind="neutral"
/>
<Part title="Purchase" text={transaction.info.summary} kind="neutral" />
<Part
title="Receipt"
text={`#${transaction.info.orderId}`}
kind="neutral"
/>
<div>
{transaction.info.products && transaction.info.products.length > 0 &&
<ListOfProducts>
{transaction.info.products.map((p, k) => <RowBorderGray key={k}>
<a href="#" onClick={showLargePic}>
<img src={p.image ? p.image : emptyImg} />
</a>
<div>
{p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
<div>{p.description}</div>
</div>
</RowBorderGray>)}
</ListOfProducts>
}
</div>
</TransactionTemplate>
<div>
{transaction.info.products && transaction.info.products.length > 0 && (
<ListOfProducts>
{transaction.info.products.map((p, k) => (
<RowBorderGray key={k}>
<a href="#" onClick={showLargePic}>
<img src={p.image ? p.image : emptyImg} />
</a>
<div>
{p.quantity && p.quantity > 0 && (
<SmallLightText>
x {p.quantity} {p.unit}
</SmallLightText>
)}
<div>{p.description}</div>
</div>
</RowBorderGray>
))}
</ListOfProducts>
)}
</div>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
return <TransactionTemplate>
<h2>Deposit </h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part big title="Total deposit" text={amountToString(transaction.amountEffective)} kind='negative' />
<Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
</TransactionTemplate>
).amount;
return (
<TransactionTemplate>
<h2>Deposit </h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
big
title="Total deposit"
text={amountToString(transaction.amountEffective)}
kind="negative"
/>
<Part
big
title="Purchase amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
return <TransactionTemplate>
<h2>Refresh</h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part big title="Total refresh" text={amountToString(transaction.amountEffective)} kind='negative' />
<Part big title="Refresh amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
</TransactionTemplate>
).amount;
return (
<TransactionTemplate>
<h2>Refresh</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
big
title="Total refresh"
text={amountToString(transaction.amountEffective)}
kind="negative"
/>
<Part
big
title="Refresh amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Tip) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
return <TransactionTemplate>
<h2>Tip</h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part big title="Total tip" text={amountToString(transaction.amountEffective)} kind='positive' />
<Part big title="Received amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
</TransactionTemplate>
).amount;
return (
<TransactionTemplate>
<h2>Tip</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
big
title="Total tip"
text={amountToString(transaction.amountEffective)}
kind="positive"
/>
<Part
big
title="Received amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
const fee = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount
return <TransactionTemplate>
<h2>Refund</h2>
<div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
<br />
<Part big title="Total refund" text={amountToString(transaction.amountEffective)} kind='positive' />
<Part big title="Refund amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
<Part big title="Fee" text={amountToString(fee)} kind='negative' />
<Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
<Part title="Purchase" text={transaction.info.summary} kind='neutral' />
<Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
).amount;
return (
<TransactionTemplate>
<h2>Refund</h2>
<div>
{transaction.timestamp.t_ms === "never"
? "never"
: format(transaction.timestamp.t_ms, "dd MMMM yyyy, HH:mm")}
</div>
<br />
<Part
big
title="Total refund"
text={amountToString(transaction.amountEffective)}
kind="positive"
/>
<Part
big
title="Refund amount"
text={amountToString(transaction.amountRaw)}
kind="neutral"
/>
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
<Part
title="Merchant"
text={transaction.info.merchant.name}
kind="neutral"
/>
<Part title="Purchase" text={transaction.info.summary} kind="neutral" />
<Part
title="Receipt"
text={`#${transaction.info.orderId}`}
kind="neutral"
/>
<p>
{transaction.info.summary}
</p>
<div>
{transaction.info.products && transaction.info.products.length > 0 &&
<ListOfProducts>
{transaction.info.products.map((p, k) => <RowBorderGray key={k}>
<a href="#" onClick={showLargePic}>
<img src={p.image ? p.image : emptyImg} />
</a>
<div>
{p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
<div>{p.description}</div>
</div>
</RowBorderGray>)}
</ListOfProducts>
}
</div>
</TransactionTemplate>
<p>{transaction.info.summary}</p>
<div>
{transaction.info.products && transaction.info.products.length > 0 && (
<ListOfProducts>
{transaction.info.products.map((p, k) => (
<RowBorderGray key={k}>
<a href="#" onClick={showLargePic}>
<img src={p.image ? p.image : emptyImg} />
</a>
<div>
{p.quantity && p.quantity > 0 && (
<SmallLightText>
x {p.quantity} {p.unit}
</SmallLightText>
)}
<div>{p.description}</div>
</div>
</RowBorderGray>
))}
</ListOfProducts>
)}
</div>
</TransactionTemplate>
);
}
return <div></div>
return <div></div>;
}

View File

@ -15,16 +15,15 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { View as TestedComponent } from './Welcome';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../test-utils";
import { View as TestedComponent } from "./Welcome";
export default {
title: 'wallet/welcome',
title: "wallet/welcome",
component: TestedComponent,
};
@ -32,11 +31,11 @@ export const Normal = createExample(TestedComponent, {
permissionsEnabled: true,
diagnostics: {
errors: [],
walletManifestVersion: '1.0',
walletManifestDisplayVersion: '1.0',
walletManifestVersion: "1.0",
walletManifestDisplayVersion: "1.0",
firefoxIdbProblem: false,
dbOutdated: false,
}
},
});
export const TimedoutDiagnostics = createExample(TestedComponent, {
@ -47,4 +46,3 @@ export const TimedoutDiagnostics = createExample(TestedComponent, {
export const RunningDiagnostics = createExample(TestedComponent, {
permissionsEnabled: false,
});

View File

@ -27,43 +27,55 @@ 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 } from "preact";
export function WelcomePage() {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
const [diagnostics, timedOut] = useDiagnostics()
return <View
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
diagnostics={diagnostics} timedOut={timedOut}
/>
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const [diagnostics, timedOut] = useDiagnostics();
return (
<View
permissionsEnabled={permissionsEnabled}
togglePermissions={togglePermissions}
diagnostics={diagnostics}
timedOut={timedOut}
/>
);
}
export interface ViewProps {
permissionsEnabled: boolean,
togglePermissions: () => void,
diagnostics: WalletDiagnostics | undefined,
timedOut: boolean,
permissionsEnabled: boolean;
togglePermissions: () => void;
diagnostics: WalletDiagnostics | undefined;
timedOut: boolean;
}
export function View({ permissionsEnabled, togglePermissions, diagnostics, timedOut }: ViewProps): JSX.Element {
return (<WalletBox>
<h1>Browser Extension Installed!</h1>
<div>
<p>Thank you for installing the wallet.</p>
<Diagnostics diagnostics={diagnostics} timedOut={timedOut} />
<h2>Permissions</h2>
<Checkbox label="Automatically open wallet based on page content"
name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
enabled={permissionsEnabled} onToggle={togglePermissions}
/>
<h2>Next Steps</h2>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Try the demo »
</a>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Learn how to top up your wallet balance »
</a>
</div>
</WalletBox>
export function View({
permissionsEnabled,
togglePermissions,
diagnostics,
timedOut,
}: ViewProps): JSX.Element {
return (
<WalletBox>
<h1>Browser Extension Installed!</h1>
<div>
<p>Thank you for installing the wallet.</p>
<Diagnostics diagnostics={diagnostics} timedOut={timedOut} />
<h2>Permissions</h2>
<Checkbox
label="Automatically open wallet based on page content"
name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
enabled={permissionsEnabled}
onToggle={togglePermissions}
/>
<h2>Next Steps</h2>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Try the demo »
</a>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
Learn how to top up your wallet balance »
</a>
</div>
</WalletBox>
);
}

View File

@ -21,7 +21,7 @@
*/
import { setupI18n } from "@gnu-taler/taler-util";
import { createHashHistory } from 'history';
import { createHashHistory } from "history";
import { Fragment, h, render } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
@ -29,22 +29,19 @@ import { LogoHeader } from "./components/LogoHeader";
import { DevContextProvider } from "./context/devContext";
import { PayPage } from "./cta/Pay";
import { RefundPage } from "./cta/Refund";
import { TipPage } from './cta/Tip';
import { TipPage } from "./cta/Tip";
import { WithdrawPage } from "./cta/Withdraw";
import { strings } from "./i18n/strings";
import {
Pages, WalletNavBar
} from "./NavigationBar";
import { Pages, WalletNavBar } from "./NavigationBar";
import { BalancePage } from "./wallet/BalancePage";
import { HistoryPage } from "./wallet/History";
import { SettingsPage } from "./wallet/Settings";
import { TransactionPage } from './wallet/Transaction';
import { TransactionPage } from "./wallet/Transaction";
import { WelcomePage } from "./wallet/Welcome";
import { BackupPage } from './wallet/BackupPage';
import { BackupPage } from "./wallet/BackupPage";
import { DeveloperPage } from "./popup/Debug.js";
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js";
function main(): void {
try {
const container = document.getElementById("container");
@ -69,51 +66,86 @@ if (document.readyState === "loading") {
}
function withLogoAndNavBar(Component: any) {
return (props: any) => <Fragment>
<LogoHeader />
<WalletNavBar />
<Component {...props} />
</Fragment>
return (props: any) => (
<Fragment>
<LogoHeader />
<WalletNavBar />
<Component {...props} />
</Fragment>
);
}
function Application() {
return <div>
<DevContextProvider>
<Router history={createHashHistory()} >
return (
<div>
<DevContextProvider>
<Router history={createHashHistory()}>
<Route
path={Pages.welcome}
component={withLogoAndNavBar(WelcomePage)}
/>
<Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} />
<Route
path={Pages.history}
component={withLogoAndNavBar(HistoryPage)}
/>
<Route
path={Pages.transaction}
component={withLogoAndNavBar(TransactionPage)}
/>
<Route
path={Pages.balance}
component={withLogoAndNavBar(BalancePage)}
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
/>
<Route
path={Pages.settings}
component={withLogoAndNavBar(SettingsPage)}
/>
<Route
path={Pages.backup}
component={withLogoAndNavBar(BackupPage)}
/>
<Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} />
<Route path={Pages.transaction} component={withLogoAndNavBar(TransactionPage)} />
<Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)}
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
/>
<Route path={Pages.settings} component={withLogoAndNavBar(SettingsPage)} />
<Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} />
<Route
path={Pages.manual_withdraw}
component={withLogoAndNavBar(ManualWithdrawPage)}
/>
<Route path={Pages.manual_withdraw} component={withLogoAndNavBar(ManualWithdrawPage)} />
<Route
path={Pages.reset_required}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.payback}
component={() => <div>no yet implemented</div>}
/>
<Route
path={Pages.return_coins}
component={() => <div>no yet implemented</div>}
/>
<Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} />
<Route path={Pages.payback} component={() => <div>no yet implemented</div>} />
<Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} />
<Route
path={Pages.dev}
component={withLogoAndNavBar(DeveloperPage)}
/>
<Route path={Pages.dev} component={withLogoAndNavBar(DeveloperPage)} />
{/** call to action */}
<Route path={Pages.pay} component={PayPage} />
<Route path={Pages.refund} component={RefundPage} />
<Route path={Pages.tips} component={TipPage} />
<Route path={Pages.withdraw} component={WithdrawPage} />
{/** call to action */}
<Route path={Pages.pay} component={PayPage} />
<Route path={Pages.refund} component={RefundPage} />
<Route path={Pages.tips} component={TipPage} />
<Route path={Pages.withdraw} component={WithdrawPage} />
<Route default component={Redirect} to={Pages.history} />
</Router>
</DevContextProvider>
</div>
<Route default component={Redirect} to={Pages.history} />
</Router>
</DevContextProvider>
</div>
);
}
function Redirect({ to }: { to: string }): null {
useEffect(() => {
route(to, true)
})
return null
route(to, true);
});
return null;
}

View File

@ -47,7 +47,12 @@ import {
AddExchangeRequest,
GetExchangeTosResult,
} from "@gnu-taler/taler-util";
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core";
import {
AddBackupProviderRequest,
BackupProviderState,
OperationFailedError,
RemoveBackupProviderRequest,
} from "@gnu-taler/taler-wallet-core";
import { BackupInfo } from "@gnu-taler/taler-wallet-core";
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
@ -149,78 +154,89 @@ interface CurrencyInfo {
pub: string;
}
interface ListOfKnownCurrencies {
auditors: CurrencyInfo[],
exchanges: CurrencyInfo[],
auditors: CurrencyInfo[];
exchanges: CurrencyInfo[];
}
/**
* Get a list of currencies from known auditors and exchanges
*/
export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
return callBackend("listCurrencies", {}).then(result => {
console.log("result list", result)
const auditors = result.trustedAuditors.map((a: Record<string, string>) => ({
name: a.currency,
baseUrl: a.auditorBaseUrl,
pub: a.auditorPub,
}))
const exchanges = result.trustedExchanges.map((a: Record<string, string>) => ({
name: a.currency,
baseUrl: a.exchangeBaseUrl,
pub: a.exchangeMasterPub,
}))
return { auditors, exchanges }
return callBackend("listCurrencies", {}).then((result) => {
console.log("result list", result);
const auditors = result.trustedAuditors.map(
(a: Record<string, string>) => ({
name: a.currency,
baseUrl: a.auditorBaseUrl,
pub: a.auditorPub,
}),
);
const exchanges = result.trustedExchanges.map(
(a: Record<string, string>) => ({
name: a.currency,
baseUrl: a.exchangeBaseUrl,
pub: a.exchangeMasterPub,
}),
);
return { auditors, exchanges };
});
}
export function listExchanges(): Promise<ExchangesListRespose> {
return callBackend("listExchanges", {})
return callBackend("listExchanges", {});
}
/**
* Get information about the current state of wallet backups.
*/
export function getBackupInfo(): Promise<BackupInfo> {
return callBackend("getBackupInfo", {})
return callBackend("getBackupInfo", {});
}
/**
* Add a backup provider and activate it
*/
export function addBackupProvider(backupProviderBaseUrl: string, name: string): Promise<void> {
export function addBackupProvider(
backupProviderBaseUrl: string,
name: string,
): Promise<void> {
return callBackend("addBackupProvider", {
backupProviderBaseUrl, activate: true, name
} as AddBackupProviderRequest)
backupProviderBaseUrl,
activate: true,
name,
} as AddBackupProviderRequest);
}
export function setWalletDeviceId(walletDeviceId: string): Promise<void> {
return callBackend("setWalletDeviceId", {
walletDeviceId
} as SetWalletDeviceIdRequest)
walletDeviceId,
} as SetWalletDeviceIdRequest);
}
export function syncAllProviders(): Promise<void> {
return callBackend("runBackupCycle", {})
return callBackend("runBackupCycle", {});
}
export function syncOneProvider(url: string): Promise<void> {
return callBackend("runBackupCycle", { providers: [url] })
return callBackend("runBackupCycle", { providers: [url] });
}
export function removeProvider(url: string): Promise<void> {
return callBackend("removeBackupProvider", { provider: url } as RemoveBackupProviderRequest)
return callBackend("removeBackupProvider", {
provider: url,
} as RemoveBackupProviderRequest);
}
export function extendedProvider(url: string): Promise<void> {
return callBackend("extendBackupProvider", { provider: url })
return callBackend("extendBackupProvider", { provider: url });
}
/**
* Retry a transaction
* @param transactionId
* @returns
* @param transactionId
* @returns
*/
export function retryTransaction(transactionId: string): Promise<void> {
return callBackend("retryTransaction", {
transactionId
transactionId,
} as RetryTransactionRequest);
}
@ -229,7 +245,7 @@ export function retryTransaction(transactionId: string): Promise<void> {
*/
export function deleteTransaction(transactionId: string): Promise<void> {
return callBackend("deleteTransaction", {
transactionId
transactionId,
} as DeleteTransactionRequest);
}
@ -264,29 +280,30 @@ export function acceptWithdrawal(
/**
* Create a reserve into the exchange that expect the amount indicated
* @param exchangeBaseUrl
* @param amount
* @returns
* @param exchangeBaseUrl
* @param amount
* @returns
*/
export function acceptManualWithdrawal(
exchangeBaseUrl: string,
amount: string,
): Promise<AcceptManualWithdrawalResult> {
return callBackend("acceptManualWithdrawal", {
amount, exchangeBaseUrl
amount,
exchangeBaseUrl,
});
}
export function setExchangeTosAccepted(
exchangeBaseUrl: string,
etag: string | undefined
etag: string | undefined,
): Promise<void> {
return callBackend("setExchangeTosAccepted", {
exchangeBaseUrl, etag
} as AcceptExchangeTosRequest)
exchangeBaseUrl,
etag,
} as AcceptExchangeTosRequest);
}
/**
* Get diagnostics information
*/
@ -319,7 +336,6 @@ export function getWithdrawalDetailsForUri(
return callBackend("getWithdrawalDetailsForUri", req);
}
/**
* Get diagnostics information
*/
@ -333,17 +349,15 @@ export function getExchangeTos(
acceptedFormat: string[],
): Promise<GetExchangeTosResult> {
return callBackend("getExchangeTos", {
exchangeBaseUrl, acceptedFormat
exchangeBaseUrl,
acceptedFormat,
});
}
export function addExchange(
req: AddExchangeRequest,
): Promise<void> {
export function addExchange(req: AddExchangeRequest): Promise<void> {
return callBackend("addExchange", req);
}
export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
return callBackend("prepareTip", req);
}

View File

@ -1,9 +1,13 @@
{
"compilerOptions": {
"composite": true,
"lib": ["es6", "DOM"],
"jsx": "react-jsx",
"jsxImportSource": "preact",
"lib": [
"es6",
"DOM"
],
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */
"jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
"moduleResolution": "Node",
"module": "ESNext",
"target": "ES6",
@ -16,7 +20,9 @@
"esModuleInterop": true,
"importHelpers": true,
"rootDir": "./src",
"typeRoots": ["./node_modules/@types"]
"typeRoots": [
"./node_modules/@types"
]
},
"references": [
{
@ -26,5 +32,7 @@
"path": "../taler-util/"
}
],
"include": ["src/**/*"]
}
"include": [
"src/**/*"
]
}