prettier
This commit is contained in:
parent
9692f589c6
commit
1d4815c66c
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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" }}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
`
|
||||
`;
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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",
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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(() => {
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { h } from 'preact';
|
||||
import { h } from "preact";
|
||||
|
||||
/**
|
||||
* View and edit auditors.
|
||||
|
@ -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">
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { h } from 'preact';
|
||||
import { h } from "preact";
|
||||
/**
|
||||
* Return coins to own bank account.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}, []);
|
||||
|
@ -193,7 +193,7 @@ strings["es"] = {
|
||||
"Order redirected": [""],
|
||||
"Payment aborted": [""],
|
||||
"Payment Sent": [""],
|
||||
"Backup": ["Resguardo"],
|
||||
Backup: ["Resguardo"],
|
||||
"Order accepted": [""],
|
||||
"Reserve balance updated": [""],
|
||||
"Payment refund": [""],
|
||||
|
@ -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: [],
|
||||
});
|
||||
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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> -></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>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i18n.Translate>storage</i18n.Translate></td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></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> -></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>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<i18n.Translate>storage</i18n.Translate>
|
||||
</td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onBack}><i18n.Translate> < back</i18n.Translate></Button>
|
||||
<Button onClick={onBack}>
|
||||
<i18n.Translate> < 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 "";
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);" });
|
||||
}
|
||||
|
@ -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: [],
|
||||
});
|
||||
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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> -></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>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i18n.Translate>storage</i18n.Translate></td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></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> -></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>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<i18n.Translate>storage</i18n.Translate>
|
||||
</td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onBack}><i18n.Translate> < back</i18n.Translate></Button>
|
||||
<Button onClick={onBack}>
|
||||
<i18n.Translate> < 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 "";
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -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>←</FontIcon></ButtonBox>
|
||||
<ButtonBox onClick={onBack}>
|
||||
<FontIcon>←</FontIcon>
|
||||
</ButtonBox>
|
||||
<div />
|
||||
</footer>
|
||||
</WalletBox>
|
||||
|
@ -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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>←</FontIcon> </i18n.Translate></ButtonBox>
|
||||
<div>
|
||||
{transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null}
|
||||
<ButtonBoxDestructive onClick={onDelete}><i18n.Translate>🗑</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>←</FontIcon>{" "}
|
||||
</i18n.Translate>
|
||||
</ButtonBox>
|
||||
<div>
|
||||
{transaction?.error ? (
|
||||
<ButtonPrimary onClick={onRetry}>
|
||||
<i18n.Translate>retry</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
) : null}
|
||||
<ButtonBoxDestructive onClick={onDelete}>
|
||||
<i18n.Translate>🗑</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>;
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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/**/*"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user