some changes:
- simplify design to reuse more components (from wallet instead of popup) - simplify hooks (useAsyncAsHook) - updateNotification from backend now filter events by type - new balance design proposed by Belen - more information when the withdrawal is in process - manual withdrawal implementation - some bugs killed
This commit is contained in:
parent
60cfb0e78f
commit
a35604fd56
@ -18,7 +18,7 @@ import { h, Fragment } from "preact"
|
||||
import { NavBar } from '../src/NavigationBar'
|
||||
import { LogoHeader } from '../src/components/LogoHeader'
|
||||
import { TranslationProvider } from '../src/context/translation'
|
||||
|
||||
import { PopupBox, WalletBox } from '../src/components/styled'
|
||||
export const parameters = {
|
||||
controls: { expanded: true },
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
@ -58,9 +58,9 @@ export const decorators = [
|
||||
// add a fake header so it looks similar
|
||||
return <Fragment>
|
||||
<NavBar path={path} devMode={path === '/dev'} />
|
||||
<div style={{ width: 400, height: 290 }}>
|
||||
<PopupBox>
|
||||
<Story />
|
||||
</div>
|
||||
</PopupBox>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ export const decorators = [
|
||||
<link key="1" rel="stylesheet" type="text/css" href="/static/style/pure.css" />
|
||||
<link key="2" rel="stylesheet" type="text/css" href="/static/style/wallet.css" />
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (kind.startsWith('wallet')) {
|
||||
const path = /wallet(\/.*).*/.exec(kind)[1];
|
||||
@ -157,7 +157,9 @@ export const decorators = [
|
||||
</style>
|
||||
<LogoHeader />
|
||||
<NavBar path={path} devMode={path === '/dev'} />
|
||||
<Story />
|
||||
<WalletBox>
|
||||
<Story />
|
||||
</WalletBox>
|
||||
</div>
|
||||
}
|
||||
return <div>
|
||||
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 GNUnet e.V.
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index";
|
||||
|
||||
export function BalanceTable({ balances }: { balances: Balance[] }): VNode {
|
||||
const currencyFormatter = new Intl.NumberFormat("en-US");
|
||||
return (
|
||||
<TableWithRoundedRows>
|
||||
{balances.map((entry, idx) => {
|
||||
const av = Amounts.parseOrThrow(entry.available);
|
||||
|
||||
const v = currencyFormatter.format(
|
||||
av.value + av.fraction / amountFractionalBase,
|
||||
);
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td>{av.currency}</td>
|
||||
<td
|
||||
style={{
|
||||
fontSize: "2em",
|
||||
textAlign: "right",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</TableWithRoundedRows>
|
||||
);
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2019 Taler Systems SA
|
||||
|
||||
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/>
|
||||
*/
|
||||
|
||||
import { PaytoUri } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { CopiedIcon, CopyIcon } from "../svg";
|
||||
import { ButtonBox, TooltipRight } from "./styled";
|
||||
|
||||
export interface BankDetailsProps {
|
||||
payto: PaytoUri | undefined;
|
||||
exchangeBaseUrl: string;
|
||||
subject: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export function BankDetailsByPaytoType({
|
||||
payto,
|
||||
subject,
|
||||
exchangeBaseUrl,
|
||||
amount,
|
||||
}: BankDetailsProps): VNode {
|
||||
const firstPart = !payto ? undefined : !payto.isKnown ? (
|
||||
<Row name="Account" value={payto.targetPath} />
|
||||
) : payto.targetType === "x-taler-bank" ? (
|
||||
<Fragment>
|
||||
<Row name="Bank host" value={payto.host} />
|
||||
<Row name="Bank account" value={payto.account} />
|
||||
</Fragment>
|
||||
) : payto.targetType === "iban" ? (
|
||||
<Row name="IBAN" value={payto.iban} />
|
||||
) : undefined;
|
||||
return (
|
||||
<div style={{ textAlign: "left" }}>
|
||||
<p>Bank transfer details</p>
|
||||
<table>
|
||||
{firstPart}
|
||||
<Row name="Exchange" value={exchangeBaseUrl} />
|
||||
<Row name="Chosen amount" value={amount} />
|
||||
<Row name="Subject" value={subject} literal />
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
name,
|
||||
value,
|
||||
literal,
|
||||
}: {
|
||||
name: string;
|
||||
value: string;
|
||||
literal?: boolean;
|
||||
}): VNode {
|
||||
const [copied, setCopied] = useState(false);
|
||||
function copyText(): void {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [copied]);
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{!copied ? (
|
||||
<ButtonBox onClick={copyText}>
|
||||
<CopyIcon />
|
||||
</ButtonBox>
|
||||
) : (
|
||||
<TooltipRight content="Copied">
|
||||
<ButtonBox disabled>
|
||||
<CopiedIcon />
|
||||
</ButtonBox>
|
||||
</TooltipRight>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<b>{name}</b>
|
||||
</td>
|
||||
{literal ? (
|
||||
<td>
|
||||
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||
{value}
|
||||
</pre>
|
||||
</td>
|
||||
) : (
|
||||
<td>{value}</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
// need to import linaria types, otherwise compiler will complain
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import type * as Linaria from "@linaria/core";
|
||||
|
||||
import { styled } from "@linaria/react";
|
||||
@ -78,9 +80,8 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
|
||||
width: 400px;
|
||||
}
|
||||
& > section {
|
||||
padding-left: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
|
||||
padding-right: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
|
||||
// this margin will send the section up when used with a header
|
||||
padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
|
||||
|
||||
margin-bottom: auto;
|
||||
overflow: auto;
|
||||
|
||||
@ -202,6 +203,152 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
export const TableWithRoundRows = styled.table`
|
||||
border-collapse: separate;
|
||||
border-spacing: 0px 10px;
|
||||
margin-top: -10px;
|
||||
|
||||
td {
|
||||
border: solid 1px #000;
|
||||
border-style: solid none;
|
||||
padding: 10px;
|
||||
}
|
||||
td:first-child {
|
||||
border-left-style: solid;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
td:last-child {
|
||||
border-right-style: solid;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.div<{ content: string }>`
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
::before {
|
||||
position: absolute;
|
||||
z-index: 1000001;
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: darkgray;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
border: 6px solid transparent;
|
||||
|
||||
border-bottom-color: darkgray;
|
||||
}
|
||||
|
||||
::after {
|
||||
position: absolute;
|
||||
z-index: 1000001;
|
||||
padding: 0.5em 0.75em;
|
||||
font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: break-word;
|
||||
white-space: pre;
|
||||
pointer-events: none;
|
||||
content: attr(content);
|
||||
background: darkgray;
|
||||
border-radius: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TooltipBottom = styled(Tooltip)`
|
||||
::before {
|
||||
top: auto;
|
||||
right: 50%;
|
||||
bottom: -7px;
|
||||
margin-right: -6px;
|
||||
}
|
||||
::after {
|
||||
top: 100%;
|
||||
right: -50%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TooltipRight = styled(Tooltip)`
|
||||
::before {
|
||||
top: 0px;
|
||||
left: 16px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
::after {
|
||||
top: -50%;
|
||||
left: 28px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Overlay = styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const CenteredDialog = styled.div`
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
/* font-size: 50px; */
|
||||
color: black;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
cursor: initial;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
|
||||
max-height: 70%;
|
||||
|
||||
& > header {
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
font-weight: bold;
|
||||
}
|
||||
& > section {
|
||||
padding: 10px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
& > footer {
|
||||
border-top: 1px solid #dbdbdb;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Button = styled.button<{ upperCased?: boolean }>`
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
@ -217,7 +364,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
padding: 0.5em 1em;
|
||||
color: #444; /* rgba not supported (IE 8) */
|
||||
/* color: #444; rgba not supported (IE 8) */
|
||||
color: rgba(0, 0, 0, 0.8); /* rgba supported */
|
||||
border: 1px solid #999; /*IE 6/7/8*/
|
||||
border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
|
||||
@ -305,8 +452,7 @@ export const FontIcon = styled.div`
|
||||
`;
|
||||
export const ButtonBox = styled(Button)`
|
||||
padding: 0.5em;
|
||||
width: fit-content;
|
||||
height: 2em;
|
||||
font-size: x-small;
|
||||
|
||||
& > ${FontIcon} {
|
||||
width: 1em;
|
||||
@ -320,6 +466,8 @@ export const ButtonBox = styled(Button)`
|
||||
border-radius: 4px;
|
||||
border-color: black;
|
||||
color: black;
|
||||
/* -webkit-border-horizontal-spacing: 0px;
|
||||
-webkit-border-vertical-spacing: 0px; */
|
||||
`;
|
||||
|
||||
const ButtonVariant = styled(Button)`
|
||||
@ -377,6 +525,7 @@ export const Centered = styled.div`
|
||||
margin-top: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Row = styled.div`
|
||||
display: flex;
|
||||
margin: 0.5em 0;
|
||||
@ -566,6 +715,12 @@ export const ErrorBox = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const InfoBox = styled(ErrorBox)`
|
||||
color: black;
|
||||
background-color: #d1e7dd;
|
||||
border-color: #badbcc;
|
||||
`;
|
||||
|
||||
export const SuccessBox = styled(ErrorBox)`
|
||||
color: #0f5132;
|
||||
background-color: #d1e7dd;
|
||||
|
@ -49,7 +49,7 @@ import {
|
||||
WalletAction,
|
||||
WarningBox,
|
||||
} from "../components/styled";
|
||||
import { useBalances } from "../hooks/useBalances";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface Props {
|
||||
@ -109,7 +109,7 @@ export function PayPage({ talerPayUri }: Props): VNode {
|
||||
);
|
||||
const [payErrMsg, setPayErrMsg] = useState<string | undefined>(undefined);
|
||||
|
||||
const balance = useBalances();
|
||||
const balance = useAsyncAsHook(wxApi.getBalance);
|
||||
const balanceWithoutError = balance?.hasError
|
||||
? []
|
||||
: balance?.response.balances || [];
|
||||
|
@ -13,7 +13,7 @@
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
import { ExchangesListRespose } from "@gnu-taler/taler-util";
|
||||
import { ExchangesListRespose, NotificationType } from "@gnu-taler/taler-util";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
@ -29,7 +29,8 @@ interface HookError {
|
||||
|
||||
export type HookResponse<T> = HookOk<T> | HookError | undefined;
|
||||
|
||||
export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> {
|
||||
//"withdraw-group-finished"
|
||||
export function useAsyncAsHook<T>(fn: () => Promise<T>, updateOnNotification?: Array<NotificationType>): HookResponse<T> {
|
||||
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
|
||||
useEffect(() => {
|
||||
async function doAsync() {
|
||||
@ -43,6 +44,11 @@ export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> {
|
||||
}
|
||||
}
|
||||
doAsync();
|
||||
if (updateOnNotification && updateOnNotification.length > 0) {
|
||||
return wxApi.onUpdateNotification(updateOnNotification, () => {
|
||||
doAsync()
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return result;
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 GNUnet e.V.
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { BalancesResponse } from "@gnu-taler/taler-util";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface BalancesHookOk {
|
||||
hasError: false;
|
||||
response: BalancesResponse;
|
||||
}
|
||||
|
||||
interface BalancesHookError {
|
||||
hasError: true;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type BalancesHook = BalancesHookOk | BalancesHookError | undefined;
|
||||
|
||||
export function useBalances(): BalancesHook {
|
||||
const [balance, setBalance] = useState<BalancesHook>(undefined);
|
||||
useEffect(() => {
|
||||
async function checkBalance() {
|
||||
try {
|
||||
const response = await wxApi.getBalance();
|
||||
console.log("got balance", balance);
|
||||
setBalance({ hasError: false, response });
|
||||
} catch (e) {
|
||||
console.error("could not retrieve balances", e);
|
||||
if (e instanceof Error) {
|
||||
setBalance({ hasError: true, message: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
checkBalance();
|
||||
return wxApi.onUpdateNotification(checkBalance);
|
||||
}, []);
|
||||
|
||||
return balance;
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { 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",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
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",
|
||||
},
|
||||
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,
|
||||
},
|
||||
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: [],
|
||||
});
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 GNUnet e.V.
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { i18n, Timestamp } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ProviderInfo,
|
||||
ProviderPaymentStatus,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import {
|
||||
differenceInMonths,
|
||||
formatDuration,
|
||||
intervalToDuration,
|
||||
} from "date-fns";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import {
|
||||
BoldLight,
|
||||
ButtonPrimary,
|
||||
ButtonSuccess,
|
||||
Centered,
|
||||
CenteredBoldText,
|
||||
CenteredText,
|
||||
PopupBox,
|
||||
RowBorderGray,
|
||||
SmallLightText,
|
||||
SmallText,
|
||||
} from "../components/styled";
|
||||
import { useBackupStatus } from "../hooks/useBackupStatus";
|
||||
import { Pages } from "../NavigationBar";
|
||||
|
||||
interface Props {
|
||||
onAddProvider: () => void;
|
||||
}
|
||||
|
||||
export function BackupPage({ onAddProvider }: Props): VNode {
|
||||
const status = useBackupStatus();
|
||||
if (!status) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
return (
|
||||
<BackupView
|
||||
providers={status.providers}
|
||||
onAddProvider={onAddProvider}
|
||||
onSyncAll={status.sync}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
providers: ProviderInfo[];
|
||||
onAddProvider: () => void;
|
||||
onSyncAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function BackupView({
|
||||
providers,
|
||||
onAddProvider,
|
||||
onSyncAll,
|
||||
}: ViewProps): VNode {
|
||||
return (
|
||||
<PopupBox>
|
||||
<section>
|
||||
{providers.map((provider, idx) => (
|
||||
<BackupLayout
|
||||
key={idx}
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</PopupBox>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransactionLayoutProps {
|
||||
status: ProviderPaymentStatus;
|
||||
timestamp?: Timestamp;
|
||||
title: string;
|
||||
id: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
function BackupLayout(props: TransactionLayoutProps): VNode {
|
||||
const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
|
||||
const dateStr = date?.toLocaleString([], {
|
||||
dateStyle: "medium",
|
||||
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>
|
||||
|
||||
{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} />
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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)";
|
||||
}
|
||||
|
||||
function daysUntil(d: Timestamp) {
|
||||
if (d.t_ms === "never") return undefined;
|
||||
const duration = intervalToDuration({
|
||||
start: d.t_ms,
|
||||
end: new Date(),
|
||||
});
|
||||
const str = formatDuration(duration, {
|
||||
delimiter: ", ",
|
||||
format: [
|
||||
duration?.years
|
||||
? "years"
|
||||
: duration?.months
|
||||
? "months"
|
||||
: duration?.days
|
||||
? "days"
|
||||
: duration.hours
|
||||
? "hours"
|
||||
: "minutes",
|
||||
],
|
||||
});
|
||||
return `${str}`;
|
||||
}
|
@ -158,7 +158,7 @@ export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
|
||||
requiresUserInput: false,
|
||||
},
|
||||
{
|
||||
available: "COL:2000",
|
||||
available: "TESTKUDOS:2000",
|
||||
hasPendingTransactions: false,
|
||||
pendingIncoming: "USD:0",
|
||||
pendingOutgoing: "USD:0",
|
||||
|
@ -14,194 +14,77 @@
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import {
|
||||
amountFractionalBase,
|
||||
Amounts,
|
||||
Balance,
|
||||
i18n,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import {
|
||||
ButtonPrimary,
|
||||
ErrorBox,
|
||||
Middle,
|
||||
PopupBox,
|
||||
} from "../components/styled/index";
|
||||
import { BalancesHook, useBalances } from "../hooks/useBalances";
|
||||
import { PageLink, renderAmount } from "../renderHtml";
|
||||
import { BalancesResponse, i18n } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { BalanceTable } from "../components/BalanceTable";
|
||||
import { ButtonPrimary, ErrorBox } from "../components/styled/index";
|
||||
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { PageLink } from "../renderHtml";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function BalancePage({
|
||||
goToWalletManualWithdraw,
|
||||
}: {
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}): VNode {
|
||||
const balance = useBalances();
|
||||
const state = useAsyncAsHook(wxApi.getBalance);
|
||||
return (
|
||||
<BalanceView
|
||||
balance={balance}
|
||||
balance={state}
|
||||
Linker={PageLink}
|
||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export interface BalanceViewProps {
|
||||
balance: BalancesHook;
|
||||
balance: HookResponse<BalancesResponse>;
|
||||
Linker: typeof PageLink;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}
|
||||
|
||||
function formatPending(entry: Balance): VNode {
|
||||
let incoming: VNode | undefined;
|
||||
let payment: VNode | undefined;
|
||||
|
||||
// const available = Amounts.parseOrThrow(entry.available);
|
||||
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
|
||||
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
|
||||
|
||||
if (!Amounts.isZero(pendingIncoming)) {
|
||||
incoming = (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const l = [incoming, payment].filter((x) => x !== undefined);
|
||||
if (l.length === 0) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
if (l.length === 1) {
|
||||
return <span>{l}</span>;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{l[0]}, {l[1]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function BalanceView({
|
||||
balance,
|
||||
Linker,
|
||||
goToWalletManualWithdraw,
|
||||
}: BalanceViewProps): VNode {
|
||||
function Content(): VNode {
|
||||
if (!balance) {
|
||||
return <span />;
|
||||
}
|
||||
if (!balance) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (balance.hasError) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
if (balance.hasError) {
|
||||
return (
|
||||
<section data-expanded data-centered>
|
||||
<table style={{ width: "100%" }}>
|
||||
{balance.response.balances.map((entry, idx) => {
|
||||
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 key={idx}>
|
||||
<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>
|
||||
<Fragment>
|
||||
<ErrorBox>{balance.message}</ErrorBox>
|
||||
<p>
|
||||
Click <Linker pageName="welcome">here</Linker> for help and
|
||||
diagnostics.
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (balance.response.balances.length === 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
<p>
|
||||
<i18n.Translate>
|
||||
You have no balance to show. Need some{" "}
|
||||
<Linker pageName="/welcome">help</Linker> getting started?
|
||||
</i18n.Translate>
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupBox>
|
||||
{/* <section> */}
|
||||
<Content />
|
||||
{/* </section> */}
|
||||
<footer>
|
||||
<div />
|
||||
<Fragment>
|
||||
<section>
|
||||
<BalanceTable balances={balance.response.balances} />
|
||||
</section>
|
||||
<footer style={{ justifyContent: "space-around" }}>
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { h, VNode } from "preact";
|
||||
import { Diagnostics } from "../components/Diagnostics";
|
||||
import { useDiagnostics } from "../hooks/useDiagnostics.js";
|
||||
import { useDiagnostics } from "../hooks/useDiagnostics";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function DeveloperPage(): VNode {
|
||||
|
@ -55,6 +55,7 @@ const exampleData = {
|
||||
type: TransactionType.Withdrawal,
|
||||
exchangeBaseUrl: "http://exchange.demo.taler.net",
|
||||
withdrawalDetails: {
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
confirmed: false,
|
||||
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
|
||||
type: WithdrawalType.ManualTransfer,
|
||||
|
@ -21,18 +21,18 @@ import {
|
||||
Transaction,
|
||||
TransactionsResponse,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { PopupBox } from "../components/styled";
|
||||
import { TransactionItem } from "../components/TransactionItem";
|
||||
import { useBalances } from "../hooks/useBalances";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function HistoryPage(): VNode {
|
||||
const [transactions, setTransactions] = useState<
|
||||
TransactionsResponse | undefined
|
||||
>(undefined);
|
||||
const balance = useBalances();
|
||||
const balance = useAsyncAsHook(wxApi.getBalance);
|
||||
const balanceWithoutError = balance?.hasError
|
||||
? []
|
||||
: balance?.response.balances || [];
|
||||
@ -57,7 +57,7 @@ export function HistoryPage(): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
function amountToString(c: AmountString) {
|
||||
function amountToString(c: AmountString): string {
|
||||
const idx = c.indexOf(":");
|
||||
return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
|
||||
}
|
||||
@ -68,18 +68,18 @@ export function HistoryView({
|
||||
}: {
|
||||
list: Transaction[];
|
||||
balances: Balance[];
|
||||
}) {
|
||||
}): VNode {
|
||||
const multiCurrency = balances.length > 1;
|
||||
return (
|
||||
<PopupBox noPadding>
|
||||
<Fragment>
|
||||
{balances.length > 0 && (
|
||||
<header>
|
||||
{multiCurrency ? (
|
||||
<div class="title">
|
||||
Balance:{" "}
|
||||
<ul style={{ margin: 0 }}>
|
||||
{balances.map((b) => (
|
||||
<li>{b.available}</li>
|
||||
{balances.map((b, i) => (
|
||||
<li key={i}>{b.available}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@ -113,8 +113,10 @@ export function HistoryView({
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "darkgreen", textDecoration: "none" }}
|
||||
href={
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.extension
|
||||
? chrome.extension.getURL(`/static/wallet.html#/history`)
|
||||
? // eslint-disable-next-line no-undef
|
||||
chrome.extension.getURL(`/static/wallet.html#/history`)
|
||||
: "#"
|
||||
}
|
||||
>
|
||||
@ -122,6 +124,6 @@ export function HistoryView({
|
||||
</a>
|
||||
)}
|
||||
</footer>
|
||||
</PopupBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample } from "../test-utils";
|
||||
import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage";
|
||||
|
||||
export default {
|
||||
title: "popup/backup/confirm",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onRetry: { action: "onRetry" },
|
||||
onDelete: { action: "onDelete" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const DemoService = createExample(TestedComponent, {
|
||||
url: "https://sync.demo.taler.net/",
|
||||
provider: {
|
||||
annual_fee: "KUDOS:0.1",
|
||||
storage_limit_in_megabytes: 20,
|
||||
supported_protocol_version: "1",
|
||||
},
|
||||
});
|
||||
|
||||
export const FreeService = createExample(TestedComponent, {
|
||||
url: "https://sync.taler:9667/",
|
||||
provider: {
|
||||
annual_fee: "ARS:0",
|
||||
storage_limit_in_megabytes: 20,
|
||||
supported_protocol_version: "1",
|
||||
},
|
||||
});
|
@ -1,244 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import {
|
||||
Amounts,
|
||||
BackupBackupProviderTerms,
|
||||
canonicalizeBaseUrl,
|
||||
i18n,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { VNode, h } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { ErrorMessage } from "../components/ErrorMessage";
|
||||
import {
|
||||
Button,
|
||||
ButtonPrimary,
|
||||
Input,
|
||||
LightText,
|
||||
PopupBox,
|
||||
SmallLightText,
|
||||
} from "../components/styled/index";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface Props {
|
||||
currency: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
function getJsonIfOk(r: Response) {
|
||||
if (r.ok) {
|
||||
return r.json();
|
||||
} else {
|
||||
if (r.status >= 400 && r.status < 500) {
|
||||
throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Try another server: (${r.status}) ${
|
||||
r.statusText || "internal server error"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ProviderAddPage({ onBack }: Props): VNode {
|
||||
const [verifying, setVerifying] = useState<
|
||||
| { url: string; name: string; provider: BackupBackupProviderTerms }
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
async function getProviderInfo(
|
||||
url: string,
|
||||
): Promise<BackupBackupProviderTerms> {
|
||||
return fetch(`${url}config`)
|
||||
.catch((e) => {
|
||||
throw new Error(`Network error`);
|
||||
})
|
||||
.then(getJsonIfOk);
|
||||
}
|
||||
|
||||
if (!verifying) {
|
||||
return (
|
||||
<SetUrlView
|
||||
onCancel={onBack}
|
||||
onVerify={(url) => getProviderInfo(url)}
|
||||
onConfirm={(url, name) =>
|
||||
getProviderInfo(url)
|
||||
.then((provider) => {
|
||||
setVerifying({ url, name, provider });
|
||||
})
|
||||
.catch((e) => e.message)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConfirmProviderView
|
||||
provider={verifying.provider}
|
||||
url={verifying.url}
|
||||
onCancel={() => {
|
||||
setVerifying(undefined);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
wxApi.addBackupProvider(verifying.url, verifying.name).then(onBack);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SetUrlViewProps {
|
||||
initialValue?: string;
|
||||
onCancel: () => void;
|
||||
onVerify: (s: string) => Promise<BackupBackupProviderTerms | undefined>;
|
||||
onConfirm: (url: string, name: string) => Promise<string | undefined>;
|
||||
withError?: string;
|
||||
}
|
||||
|
||||
export function SetUrlView({
|
||||
initialValue,
|
||||
onCancel,
|
||||
onVerify,
|
||||
onConfirm,
|
||||
withError,
|
||||
}: SetUrlViewProps) {
|
||||
const [value, setValue] = useState<string>(initialValue || "");
|
||||
const [urlError, setUrlError] = useState(false);
|
||||
const [name, setName] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(withError);
|
||||
useEffect(() => {
|
||||
try {
|
||||
const url = canonicalizeBaseUrl(value);
|
||||
onVerify(url)
|
||||
.then((r) => {
|
||||
setUrlError(false);
|
||||
setName(new URL(url).hostname);
|
||||
})
|
||||
.catch(() => {
|
||||
setUrlError(true);
|
||||
setName(undefined);
|
||||
});
|
||||
} catch {
|
||||
setUrlError(true);
|
||||
setName(undefined);
|
||||
}
|
||||
}, [value]);
|
||||
return (
|
||||
<PopupBox>
|
||||
<section>
|
||||
<h1> Add backup provider</h1>
|
||||
<ErrorMessage
|
||||
title={error && "Could not get provider information"}
|
||||
description={error}
|
||||
/>
|
||||
<LightText> Backup providers may charge for their service</LightText>
|
||||
<p>
|
||||
<Input invalid={urlError}>
|
||||
<label>URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</Input>
|
||||
<Input>
|
||||
<label>Name</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled={name === undefined}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</Input>
|
||||
</p>
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onCancel}>
|
||||
<i18n.Translate> < Back</i18n.Translate>
|
||||
</Button>
|
||||
<ButtonPrimary
|
||||
disabled={!value && !urlError}
|
||||
onClick={() => {
|
||||
const url = canonicalizeBaseUrl(value);
|
||||
return onConfirm(url, name!).then((r) =>
|
||||
r ? setError(r) : undefined,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i18n.Translate>Next</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ConfirmProviderViewProps {
|
||||
provider: BackupBackupProviderTerms;
|
||||
url: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
export function ConfirmProviderView({
|
||||
url,
|
||||
provider,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: ConfirmProviderViewProps) {
|
||||
const [accepted, setAccepted] = useState(false);
|
||||
|
||||
return (
|
||||
<PopupBox>
|
||||
<section>
|
||||
<h1>Review terms of service</h1>
|
||||
<div>
|
||||
Provider URL:{" "}
|
||||
<a href={url} target="_blank">
|
||||
{url}
|
||||
</a>
|
||||
</div>
|
||||
<SmallLightText>
|
||||
Please review and accept this provider's terms of service
|
||||
</SmallLightText>
|
||||
<h2>1. Pricing</h2>
|
||||
<p>
|
||||
{Amounts.isZero(provider.annual_fee)
|
||||
? "free of charge"
|
||||
: `${provider.annual_fee} per year of service`}
|
||||
</p>
|
||||
<h2>2. Storage</h2>
|
||||
<p>
|
||||
{provider.storage_limit_in_megabytes} megabytes of storage per year of
|
||||
service
|
||||
</p>
|
||||
<Checkbox
|
||||
label="Accept terms of service"
|
||||
name="terms"
|
||||
onToggle={() => setAccepted((old) => !old)}
|
||||
enabled={accepted}
|
||||
/>
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onCancel}>
|
||||
<i18n.Translate> < Back</i18n.Translate>
|
||||
</Button>
|
||||
<ButtonPrimary disabled={!accepted} onClick={onConfirm}>
|
||||
<i18n.Translate>Add provider</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
);
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { createExample } from "../test-utils";
|
||||
import { SetUrlView as TestedComponent } from "./ProviderAddPage";
|
||||
|
||||
export default {
|
||||
title: "popup/backup/add",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onRetry: { action: "onRetry" },
|
||||
onDelete: { action: "onDelete" },
|
||||
onBack: { action: "onBack" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Initial = createExample(TestedComponent, {});
|
||||
|
||||
export const WithValue = createExample(TestedComponent, {
|
||||
initialValue: "sync.demo.taler.net",
|
||||
});
|
||||
|
||||
export const WithConnectionError = createExample(TestedComponent, {
|
||||
withError: "Network error",
|
||||
});
|
||||
|
||||
export const WithClientError = createExample(TestedComponent, {
|
||||
withError: "URL may not be right: (404) Not Found",
|
||||
});
|
||||
|
||||
export const WithServerError = createExample(TestedComponent, {
|
||||
withError: "Try another server: (500) Internal Server Error",
|
||||
});
|
@ -1,235 +0,0 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 Taler Systems S.A.
|
||||
|
||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
|
||||
import { createExample } from "../test-utils";
|
||||
import { ProviderView as TestedComponent } from "./ProviderDetailPage";
|
||||
|
||||
export default {
|
||||
title: "popup/backup/details",
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
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,
|
||||
},
|
||||
paymentProposalIds: [
|
||||
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
|
||||
],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
lastAttemptedBackupTimestamp: {
|
||||
t_ms: 1625063925078,
|
||||
},
|
||||
paymentProposalIds: [
|
||||
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
|
||||
],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
},
|
||||
lastError: {
|
||||
code: 2002,
|
||||
details: "details",
|
||||
hint: "error hint from the server",
|
||||
message: "message",
|
||||
},
|
||||
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,
|
||||
},
|
||||
paymentProposalIds: [
|
||||
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
|
||||
],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
},
|
||||
backupProblem: {
|
||||
type: "backup-unreadable",
|
||||
},
|
||||
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,
|
||||
},
|
||||
paymentProposalIds: [
|
||||
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
|
||||
],
|
||||
paymentStatus: {
|
||||
type: ProviderPaymentType.Paid,
|
||||
paidUntil: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
},
|
||||
backupProblem: {
|
||||
type: "backup-conflicting-device",
|
||||
myDeviceId: "my-device-id",
|
||||
otherDeviceId: "other-device-id",
|
||||
backupTimestamp: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
paidUntil: {
|
||||
t_ms: 1656599921000,
|
||||
},
|
||||
newTerms: {
|
||||
annualFee: "EUR:10",
|
||||
storageLimitInMegabytes: 8,
|
||||
supportedProtocolVersion: "0.0",
|
||||
},
|
||||
oldTerms: {
|
||||
annualFee: "EUR:0.1",
|
||||
storageLimitInMegabytes: 16,
|
||||
supportedProtocolVersion: "0.0",
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
annualFee: "EUR:0.1",
|
||||
storageLimitInMegabytes: 16,
|
||||
supportedProtocolVersion: "0.0",
|
||||
},
|
||||
},
|
||||
});
|
@ -1,278 +0,0 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
(C) 2016 GNUnet e.V.
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; either version 3, or (at your option) any later version.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { i18n, Timestamp } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ProviderInfo,
|
||||
ProviderPaymentStatus,
|
||||
ProviderPaymentType,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import { format, formatDuration, intervalToDuration } from "date-fns";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { ErrorMessage } from "../components/ErrorMessage";
|
||||
import {
|
||||
Button,
|
||||
ButtonDestructive,
|
||||
ButtonPrimary,
|
||||
PaymentStatus,
|
||||
PopupBox,
|
||||
SmallLightText,
|
||||
} from "../components/styled";
|
||||
import { useProviderStatus } from "../hooks/useProviderStatus";
|
||||
|
||||
interface Props {
|
||||
pid: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
|
||||
const status = useProviderStatus(pid);
|
||||
if (!status) {
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>Loading...</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!status.info) {
|
||||
onBack();
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<ProviderView
|
||||
info={status.info}
|
||||
onSync={status.sync}
|
||||
onDelete={() => status.remove().then(onBack)}
|
||||
onBack={onBack}
|
||||
onExtend={() => {
|
||||
null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
info: ProviderInfo;
|
||||
onDelete: () => void;
|
||||
onSync: () => void;
|
||||
onBack: () => void;
|
||||
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;
|
||||
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>
|
||||
</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>{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>
|
||||
)}
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onBack}>
|
||||
<i18n.Translate> < back</i18n.Translate>
|
||||
</Button>
|
||||
<div>
|
||||
<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";
|
||||
const duration = intervalToDuration({
|
||||
start: d.t_ms,
|
||||
end: new Date(),
|
||||
});
|
||||
const str = formatDuration(duration, {
|
||||
delimiter: ", ",
|
||||
format: [
|
||||
duration?.years
|
||||
? i18n.str`years`
|
||||
: duration?.months
|
||||
? i18n.str`months`
|
||||
: duration?.days
|
||||
? i18n.str`days`
|
||||
: duration?.hours
|
||||
? i18n.str`hours`
|
||||
: duration?.minutes
|
||||
? i18n.str`minutes`
|
||||
: i18n.str`seconds`,
|
||||
],
|
||||
});
|
||||
return `synced ${str} ago`;
|
||||
}
|
||||
|
||||
function Error({ info }: { info: ProviderInfo }) {
|
||||
if (info.lastError) {
|
||||
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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case "backup-unreadable":
|
||||
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 null;
|
||||
}
|
||||
|
||||
function colorByStatus(status: ProviderPaymentType) {
|
||||
switch (status) {
|
||||
case ProviderPaymentType.InsufficientBalance:
|
||||
return "rgb(223, 117, 20)";
|
||||
case ProviderPaymentType.Unpaid:
|
||||
return "rgb(202, 60, 60)";
|
||||
case ProviderPaymentType.Paid:
|
||||
return "rgb(28, 184, 65)";
|
||||
case ProviderPaymentType.Pending:
|
||||
return "gray";
|
||||
case ProviderPaymentType.InsufficientBalance:
|
||||
return "rgb(202, 60, 60)";
|
||||
case ProviderPaymentType.TermsChanged:
|
||||
return "rgb(202, 60, 60)";
|
||||
}
|
||||
}
|
||||
|
||||
function descriptionByStatus(status: ProviderPaymentStatus) {
|
||||
switch (status.type) {
|
||||
// return i18n.str`no enough balance to make the payment`
|
||||
// return i18n.str`not paid yet`
|
||||
case ProviderPaymentType.Paid:
|
||||
case ProviderPaymentType.TermsChanged:
|
||||
if (status.paidUntil.t_ms === "never") {
|
||||
return i18n.str`service paid`;
|
||||
} else {
|
||||
return (
|
||||
<Fragment>
|
||||
<b>Backup valid until:</b>{" "}
|
||||
{format(status.paidUntil.t_ms, "dd MMM yyyy")}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
case ProviderPaymentType.Unpaid:
|
||||
case ProviderPaymentType.InsufficientBalance:
|
||||
case ProviderPaymentType.Pending:
|
||||
return "";
|
||||
}
|
||||
}
|
@ -14,26 +14,34 @@
|
||||
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 { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { EditableText } from "../components/EditableText";
|
||||
import { SelectList } from "../components/SelectList";
|
||||
import { PopupBox } from "../components/styled";
|
||||
import { ButtonPrimary } from "../components/styled";
|
||||
import { useDevContext } from "../context/devContext";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
|
||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||
import { useLang } from "../hooks/useLang";
|
||||
// import { strings as messages } from "../i18n/strings";
|
||||
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 exchangesHook = useAsyncAsHook(wxApi.listExchanges);
|
||||
|
||||
return (
|
||||
<SettingsView
|
||||
lang={lang}
|
||||
changeLang={changeLang}
|
||||
knownExchanges={
|
||||
!exchangesHook || exchangesHook.hasError
|
||||
? []
|
||||
: exchangesHook.response.exchanges
|
||||
}
|
||||
deviceName={name}
|
||||
setDeviceName={update}
|
||||
permissionsEnabled={permissionsEnabled}
|
||||
@ -53,36 +61,59 @@ export interface ViewProps {
|
||||
togglePermissions: () => void;
|
||||
developerMode: boolean;
|
||||
toggleDeveloperMode: () => void;
|
||||
knownExchanges: Array<ExchangeListItem>;
|
||||
}
|
||||
|
||||
import { strings as messages } from "../i18n/strings";
|
||||
// type LangsNames = {
|
||||
// [P in keyof typeof messages]: string;
|
||||
// };
|
||||
|
||||
type LangsNames = {
|
||||
[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]",
|
||||
};
|
||||
// const names: LangsNames = {
|
||||
// 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,
|
||||
knownExchanges,
|
||||
// lang,
|
||||
// changeLang,
|
||||
// deviceName,
|
||||
// setDeviceName,
|
||||
permissionsEnabled,
|
||||
togglePermissions,
|
||||
developerMode,
|
||||
toggleDeveloperMode,
|
||||
}: ViewProps): VNode {
|
||||
return (
|
||||
<PopupBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h2>
|
||||
<i18n.Translate>Known exchanges</i18n.Translate>
|
||||
</h2>
|
||||
{!knownExchanges || !knownExchanges.length ? (
|
||||
<div>No exchange yet!</div>
|
||||
) : (
|
||||
<Fragment>
|
||||
<table>
|
||||
{knownExchanges.map((e, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>{e.currency}</td>
|
||||
<td>
|
||||
<a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Fragment>
|
||||
)}
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary>Manage exchange</ButtonPrimary>
|
||||
</div>
|
||||
{/* <h2><i18n.Translate>Wallet</i18n.Translate></h2> */}
|
||||
{/* <SelectList
|
||||
value={lang}
|
||||
@ -124,14 +155,16 @@ export function SettingsView({
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "darkgreen", textDecoration: "none" }}
|
||||
href={
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.extension
|
||||
? chrome.extension.getURL(`/static/wallet.html#/settings`)
|
||||
? // eslint-disable-next-line no-undef
|
||||
chrome.extension.getURL(`/static/wallet.html#/settings`)
|
||||
: "#"
|
||||
}
|
||||
>
|
||||
VIEW MORE SETTINGS
|
||||
</a>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -20,12 +20,8 @@
|
||||
*/
|
||||
|
||||
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ButtonPrimary,
|
||||
ButtonSuccess,
|
||||
PopupBox,
|
||||
} from "../components/styled/index";
|
||||
import { h } from "preact";
|
||||
import { Fragment, h } from "preact";
|
||||
import { ButtonPrimary, ButtonSuccess } from "../components/styled/index";
|
||||
|
||||
export interface Props {
|
||||
url: string;
|
||||
@ -35,7 +31,7 @@ export interface Props {
|
||||
export function TalerActionFound({ url, onDismiss }: Props) {
|
||||
const uriType = classifyTalerUri(url);
|
||||
return (
|
||||
<PopupBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h1>Taler Action </h1>
|
||||
{uriType === TalerUriType.TalerPay && (
|
||||
@ -109,7 +105,7 @@ export function TalerActionFound({ url, onDismiss }: Props) {
|
||||
<div />
|
||||
<ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -25,16 +25,17 @@ import { createHashHistory } from "history";
|
||||
import { render, h } from "preact";
|
||||
import Router, { route, Route } from "preact-router";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { PopupBox } from "./components/styled";
|
||||
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 { BackupPage } from "./wallet/BackupPage";
|
||||
import { BalancePage } from "./popup/BalancePage";
|
||||
import { DeveloperPage } from "./popup/Debug";
|
||||
import { HistoryPage } from "./popup/History";
|
||||
import { ProviderAddPage } from "./popup/ProviderAddPage";
|
||||
import { ProviderDetailPage } from "./popup/ProviderDetailPage";
|
||||
import { ProviderAddPage } from "./wallet/ProviderAddPage";
|
||||
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
|
||||
import { SettingsPage } from "./popup/Settings";
|
||||
import { TalerActionFound } from "./popup/TalerActionFound";
|
||||
|
||||
@ -72,7 +73,7 @@ function Application() {
|
||||
<div>
|
||||
<DevContextProvider>
|
||||
<WalletNavBar />
|
||||
<div style={{ width: 400, height: 290 }}>
|
||||
<PopupBox>
|
||||
<Router history={createHashHistory()}>
|
||||
<Route path={Pages.dev} component={DeveloperPage} />
|
||||
|
||||
@ -128,15 +129,17 @@ function Application() {
|
||||
/>
|
||||
<Route default component={Redirect} to={Pages.balance} />
|
||||
</Router>
|
||||
</div>
|
||||
</PopupBox>
|
||||
</DevContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function goToWalletPage(page: Pages | string): null {
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.tabs.create({
|
||||
active: true,
|
||||
// eslint-disable-next-line no-undef
|
||||
url: chrome.extension.getURL(`/static/wallet.html#${page}`),
|
||||
});
|
||||
return null;
|
||||
|
40
packages/taler-wallet-webextension/src/svg/index.tsx
Normal file
40
packages/taler-wallet-webextension/src/svg/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { h, VNode } from "preact";
|
||||
|
||||
export const CopyIcon = (): VNode => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="10"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="10"
|
||||
data-view-component="true"
|
||||
class="octicon octicon-copy"
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CopiedIcon = (): VNode => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="8"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="8"
|
||||
data-view-component="true"
|
||||
class="octicon octicon-check color-fg-success"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
@ -35,7 +35,6 @@ import {
|
||||
RowBorderGray,
|
||||
SmallLightText,
|
||||
SmallText,
|
||||
WalletBox,
|
||||
} from "../components/styled";
|
||||
import { useBackupStatus } from "../hooks/useBackupStatus";
|
||||
import { Pages } from "../NavigationBar";
|
||||
@ -70,7 +69,7 @@ export function BackupView({
|
||||
onSyncAll,
|
||||
}: ViewProps): VNode {
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
{providers.map((provider, idx) => (
|
||||
<BackupLayout
|
||||
@ -106,7 +105,7 @@ export function BackupView({
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,7 +154,7 @@ function BackupLayout(props: TransactionLayoutProps): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
function ExpirationText({ until }: { until: Timestamp }) {
|
||||
function ExpirationText({ until }: { until: Timestamp }): VNode {
|
||||
return (
|
||||
<Fragment>
|
||||
<CenteredText> Expires in </CenteredText>
|
||||
@ -167,14 +166,14 @@ function ExpirationText({ until }: { until: Timestamp }) {
|
||||
);
|
||||
}
|
||||
|
||||
function colorByTimeToExpire(d: Timestamp) {
|
||||
function colorByTimeToExpire(d: Timestamp): string {
|
||||
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;
|
||||
function daysUntil(d: Timestamp): string {
|
||||
if (d.t_ms === "never") return "";
|
||||
const duration = intervalToDuration({
|
||||
start: d.t_ms,
|
||||
end: new Date(),
|
||||
|
@ -14,27 +14,23 @@
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import {
|
||||
amountFractionalBase,
|
||||
Amounts,
|
||||
Balance,
|
||||
BalancesResponse,
|
||||
i18n,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index";
|
||||
import { BalancesHook, useBalances } from "../hooks/useBalances";
|
||||
import { PageLink, renderAmount } from "../renderHtml";
|
||||
import { BalancesResponse, i18n } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { BalanceTable } from "../components/BalanceTable";
|
||||
import { ButtonPrimary, ErrorBox } from "../components/styled/index";
|
||||
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { PageLink } from "../renderHtml";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function BalancePage({
|
||||
goToWalletManualWithdraw,
|
||||
}: {
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}): VNode {
|
||||
const balance = useBalances();
|
||||
const state = useAsyncAsHook(wxApi.getBalance);
|
||||
return (
|
||||
<BalanceView
|
||||
balance={balance}
|
||||
balance={state}
|
||||
Linker={PageLink}
|
||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||
/>
|
||||
@ -42,7 +38,7 @@ export function BalancePage({
|
||||
}
|
||||
|
||||
export interface BalanceViewProps {
|
||||
balance: BalancesHook;
|
||||
balance: HookResponse<BalancesResponse>;
|
||||
Linker: typeof PageLink;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}
|
||||
@ -53,18 +49,18 @@ export function BalanceView({
|
||||
goToWalletManualWithdraw,
|
||||
}: BalanceViewProps): VNode {
|
||||
if (!balance) {
|
||||
return <span />;
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (balance.hasError) {
|
||||
return (
|
||||
<div>
|
||||
<p>{i18n.str`Error: could not retrieve balance information.`}</p>
|
||||
<Fragment>
|
||||
<ErrorBox>{balance.message}</ErrorBox>
|
||||
<p>
|
||||
Click <Linker pageName="welcome">here</Linker> for help and
|
||||
diagnostics.
|
||||
</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
if (balance.response.balances.length === 0) {
|
||||
@ -77,81 +73,17 @@ export function BalanceView({
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowBalances
|
||||
wallet={balance.response}
|
||||
onWithdraw={goToWalletManualWithdraw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function formatPending(entry: Balance): VNode {
|
||||
let incoming: VNode | undefined;
|
||||
let payment: VNode | undefined;
|
||||
|
||||
// const available = Amounts.parseOrThrow(entry.available);
|
||||
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
|
||||
// const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
|
||||
|
||||
if (!Amounts.isZero(pendingIncoming)) {
|
||||
incoming = (
|
||||
<span>
|
||||
<i18n.Translate>
|
||||
<span style={{ color: "darkgreen" }}>
|
||||
{"+"}
|
||||
{renderAmount(entry.pendingIncoming)}
|
||||
</span>{" "}
|
||||
incoming
|
||||
</i18n.Translate>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const l = [incoming, payment].filter((x) => x !== undefined);
|
||||
if (l.length === 0) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
if (l.length === 1) {
|
||||
return <span>({l})</span>;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
({l[0]}, {l[1]})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowBalances({
|
||||
wallet,
|
||||
onWithdraw,
|
||||
}: {
|
||||
wallet: BalancesResponse;
|
||||
onWithdraw: () => void;
|
||||
}): VNode {
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<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>
|
||||
<BalanceTable balances={balance.response.balances} />
|
||||
</section>
|
||||
<footer>
|
||||
<div />
|
||||
<ButtonPrimary onClick={onWithdraw}>Withdraw</ButtonPrimary>
|
||||
<footer style={{ justifyContent: "space-around" }}>
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ const exchangeList = {
|
||||
"http://exchange.tal": "EUR",
|
||||
};
|
||||
|
||||
export const WithoutAnyExchangeKnown = createExample(TestedComponent, {
|
||||
exchangeList: {},
|
||||
});
|
||||
|
||||
export const InitialState = createExample(TestedComponent, {
|
||||
exchangeList,
|
||||
});
|
||||
|
@ -19,17 +19,19 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { AmountJson, Amounts, i18n } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { ErrorMessage } from "../components/ErrorMessage";
|
||||
import { SelectList } from "../components/SelectList";
|
||||
import {
|
||||
BoldLight,
|
||||
ButtonPrimary,
|
||||
ButtonSuccess,
|
||||
Centered,
|
||||
Input,
|
||||
InputWithLabel,
|
||||
LightText,
|
||||
WalletBox,
|
||||
} from "../components/styled";
|
||||
|
||||
export interface Props {
|
||||
@ -82,11 +84,23 @@ export function CreateManualWithdraw({
|
||||
}
|
||||
|
||||
if (!initialExchange) {
|
||||
return <div>There is no known exchange where to withdraw, add one</div>;
|
||||
return (
|
||||
<Centered style={{ marginTop: 100 }}>
|
||||
<BoldLight>No exchange configured</BoldLight>
|
||||
<ButtonSuccess
|
||||
//FIXME: add exchange feature
|
||||
onClick={() => {
|
||||
null;
|
||||
}}
|
||||
>
|
||||
<i18n.Translate>Add exchange</i18n.Translate>
|
||||
</ButtonSuccess>
|
||||
</Centered>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<ErrorMessage
|
||||
title={error && "Can't create the reserve"}
|
||||
@ -145,6 +159,6 @@ export function CreateManualWithdraw({
|
||||
Start withdrawal
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ const exampleData = {
|
||||
type: TransactionType.Withdrawal,
|
||||
exchangeBaseUrl: "http://exchange.demo.taler.net",
|
||||
withdrawalDetails: {
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
confirmed: false,
|
||||
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
|
||||
type: WithdrawalType.ManualTransfer,
|
||||
|
@ -17,42 +17,37 @@
|
||||
import {
|
||||
AmountString,
|
||||
Balance,
|
||||
NotificationType,
|
||||
Transaction,
|
||||
TransactionsResponse,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { DateSeparator, WalletBox } from "../components/styled";
|
||||
import { DateSeparator } from "../components/styled";
|
||||
import { Time } from "../components/Time";
|
||||
import { TransactionItem } from "../components/TransactionItem";
|
||||
import { useBalances } from "../hooks/useBalances";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function HistoryPage(): VNode {
|
||||
const [transactions, setTransactions] = useState<
|
||||
TransactionsResponse | undefined
|
||||
>(undefined);
|
||||
const balance = useBalances();
|
||||
const balance = useAsyncAsHook(wxApi.getBalance);
|
||||
const balanceWithoutError = balance?.hasError
|
||||
? []
|
||||
: balance?.response.balances || [];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async (): Promise<void> => {
|
||||
const res = await wxApi.getTransactions();
|
||||
setTransactions(res);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
const transactionQuery = useAsyncAsHook(wxApi.getTransactions, [
|
||||
NotificationType.WithdrawGroupFinished,
|
||||
]);
|
||||
|
||||
if (!transactions) {
|
||||
if (!transactionQuery) {
|
||||
return <div>Loading ...</div>;
|
||||
}
|
||||
if (transactionQuery.hasError) {
|
||||
return <div>There was an error loading the transactions.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<HistoryView
|
||||
balances={balanceWithoutError}
|
||||
list={[...transactions.transactions].reverse()}
|
||||
list={[...transactionQuery.response.transactions].reverse()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -87,7 +82,7 @@ export function HistoryView({
|
||||
const multiCurrency = balances.length > 1;
|
||||
|
||||
return (
|
||||
<WalletBox noPadding>
|
||||
<Fragment>
|
||||
{balances.length > 0 && (
|
||||
<header>
|
||||
{balances.length === 1 && (
|
||||
@ -128,6 +123,6 @@ export function HistoryView({
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { ReserveCreated } from "./ReserveCreated.js";
|
||||
import { ReserveCreated } from "./ReserveCreated";
|
||||
import { route } from "preact-router";
|
||||
import { Pages } from "../NavigationBar.js";
|
||||
import { Pages } from "../NavigationBar";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
|
||||
export function ManualWithdrawPage(): VNode {
|
||||
@ -39,7 +39,7 @@ export function ManualWithdrawPage(): VNode {
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
|
||||
const state = useAsyncAsHook(() => wxApi.listExchanges());
|
||||
|
||||
async function doCreate(
|
||||
exchangeBaseUrl: string,
|
||||
@ -75,10 +75,13 @@ export function ManualWithdrawPage(): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
if (!knownExchangesHook || knownExchangesHook.hasError) {
|
||||
return <div>No Known exchanges</div>;
|
||||
if (!state) {
|
||||
return <div>loading...</div>;
|
||||
}
|
||||
const exchangeList = knownExchangesHook.response.exchanges.reduce(
|
||||
if (state.hasError) {
|
||||
return <div>There was an error getting the known exchanges</div>;
|
||||
}
|
||||
const exchangeList = state.response.exchanges.reduce(
|
||||
(p, c) => ({
|
||||
...p,
|
||||
[c.exchangeBaseUrl]: c.currency,
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
canonicalizeBaseUrl,
|
||||
i18n,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { VNode, h } from "preact";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { ErrorMessage } from "../components/ErrorMessage";
|
||||
@ -29,7 +29,6 @@ import {
|
||||
ButtonPrimary,
|
||||
Input,
|
||||
LightText,
|
||||
WalletBox,
|
||||
SmallLightText,
|
||||
} from "../components/styled/index";
|
||||
import * as wxApi from "../wxApi";
|
||||
@ -64,7 +63,7 @@ export function ProviderAddPage({ onBack }: Props): VNode {
|
||||
async function getProviderInfo(
|
||||
url: string,
|
||||
): Promise<BackupBackupProviderTerms> {
|
||||
return fetch(`${url}config`)
|
||||
return fetch(new URL("config", url).href)
|
||||
.catch((e) => {
|
||||
throw new Error(`Network error`);
|
||||
})
|
||||
@ -137,7 +136,7 @@ export function SetUrlView({
|
||||
}
|
||||
}, [value]);
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h1> Add backup provider</h1>
|
||||
<ErrorMessage
|
||||
@ -182,7 +181,7 @@ export function SetUrlView({
|
||||
<i18n.Translate>Next</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -201,7 +200,7 @@ export function ConfirmProviderView({
|
||||
const [accepted, setAccepted] = useState(false);
|
||||
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h1>Review terms of service</h1>
|
||||
<div>
|
||||
@ -239,6 +238,6 @@ export function ConfirmProviderView({
|
||||
<i18n.Translate>Add provider</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -28,34 +28,62 @@ import {
|
||||
ButtonPrimary,
|
||||
PaymentStatus,
|
||||
SmallLightText,
|
||||
WalletBox,
|
||||
} from "../components/styled";
|
||||
import { Time } from "../components/Time";
|
||||
import { useProviderStatus } from "../hooks/useProviderStatus";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface Props {
|
||||
pid: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
|
||||
const status = useProviderStatus(pid);
|
||||
if (!status) {
|
||||
export function ProviderDetailPage({ pid: providerURL, onBack }: Props): VNode {
|
||||
async function getProviderInfo(): Promise<ProviderInfo | null> {
|
||||
//create a first list of backup info by currency
|
||||
const status = await wxApi.getBackupInfo();
|
||||
|
||||
const providers = status.providers.filter(
|
||||
(p) => p.syncProviderBaseUrl === providerURL,
|
||||
);
|
||||
return providers.length ? providers[0] : null;
|
||||
}
|
||||
|
||||
const state = useAsyncAsHook(getProviderInfo);
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>Loading...</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!status.info) {
|
||||
if (state.hasError) {
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>
|
||||
There was an error loading the provider detail for "{providerURL}"
|
||||
</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.response === null) {
|
||||
onBack();
|
||||
return <div />;
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>
|
||||
There is not known provider with url "{providerURL}". Redirecting
|
||||
back...
|
||||
</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ProviderView
|
||||
info={status.info}
|
||||
onSync={status.sync}
|
||||
onDelete={() => status.remove().then(onBack)}
|
||||
info={state.response}
|
||||
onSync={async () => wxApi.syncOneProvider(providerURL)}
|
||||
onDelete={async () => wxApi.syncOneProvider(providerURL).then(onBack)}
|
||||
onBack={onBack}
|
||||
onExtend={() => {
|
||||
null;
|
||||
@ -84,7 +112,7 @@ export function ProviderView({
|
||||
info.paymentStatus.type === ProviderPaymentType.Paid ||
|
||||
info.paymentStatus.type === ProviderPaymentType.TermsChanged;
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<Error info={info} />
|
||||
<header>
|
||||
<h3>
|
||||
@ -167,35 +195,10 @@ export function ProviderView({
|
||||
</ButtonDestructive>
|
||||
</div>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// function daysSince(d?: Timestamp): string {
|
||||
// if (!d || d.t_ms === "never") return "never synced";
|
||||
// const duration = intervalToDuration({
|
||||
// start: d.t_ms,
|
||||
// end: new Date(),
|
||||
// });
|
||||
// const str = formatDuration(duration, {
|
||||
// delimiter: ", ",
|
||||
// format: [
|
||||
// duration?.years
|
||||
// ? i18n.str`years`
|
||||
// : duration?.months
|
||||
// ? i18n.str`months`
|
||||
// : duration?.days
|
||||
// ? i18n.str`days`
|
||||
// : duration?.hours
|
||||
// ? i18n.str`hours`
|
||||
// : duration?.minutes
|
||||
// ? i18n.str`minutes`
|
||||
// : i18n.str`seconds`,
|
||||
// ],
|
||||
// });
|
||||
// return `synced ${str} ago`;
|
||||
// }
|
||||
|
||||
function Error({ info }: { info: ProviderInfo }): VNode {
|
||||
if (info.lastError) {
|
||||
return <ErrorMessage title={info.lastError.hint} />;
|
||||
@ -234,23 +237,6 @@ function Error({ info }: { info: ProviderInfo }): VNode {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
// function colorByStatus(status: ProviderPaymentType): string {
|
||||
// switch (status) {
|
||||
// case ProviderPaymentType.InsufficientBalance:
|
||||
// return "rgb(223, 117, 20)";
|
||||
// case ProviderPaymentType.Unpaid:
|
||||
// return "rgb(202, 60, 60)";
|
||||
// case ProviderPaymentType.Paid:
|
||||
// return "rgb(28, 184, 65)";
|
||||
// case ProviderPaymentType.Pending:
|
||||
// return "gray";
|
||||
// // case ProviderPaymentType.InsufficientBalance:
|
||||
// // return "rgb(202, 60, 60)";
|
||||
// case ProviderPaymentType.TermsChanged:
|
||||
// return "rgb(202, 60, 60)";
|
||||
// }
|
||||
// }
|
||||
|
||||
function descriptionByStatus(status: ProviderPaymentStatus): VNode {
|
||||
switch (status.type) {
|
||||
// return i18n.str`no enough balance to make the payment`
|
||||
|
@ -1,18 +1,8 @@
|
||||
import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
parsePaytoUri,
|
||||
PaytoUri,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AmountJson, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType";
|
||||
import { QR } from "../components/QR";
|
||||
import {
|
||||
ButtonDestructive,
|
||||
ButtonPrimary,
|
||||
WalletBox,
|
||||
WarningBox,
|
||||
} from "../components/styled";
|
||||
import { ButtonDestructive, WarningBox } from "../components/styled";
|
||||
export interface Props {
|
||||
reservePub: string;
|
||||
payto: string;
|
||||
@ -21,92 +11,6 @@ export interface Props {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
interface BankDetailsProps {
|
||||
payto: PaytoUri;
|
||||
exchangeBaseUrl: string;
|
||||
subject: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
function Row({
|
||||
name,
|
||||
value,
|
||||
literal,
|
||||
}: {
|
||||
name: string;
|
||||
value: string;
|
||||
literal?: boolean;
|
||||
}): VNode {
|
||||
const [copied, setCopied] = useState(false);
|
||||
function copyText(): void {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
}
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
}, [copied]);
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{!copied ? (
|
||||
<ButtonPrimary small onClick={copyText}>
|
||||
Copy
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<ButtonPrimary small disabled>
|
||||
Copied
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<b>{name}</b>
|
||||
</td>
|
||||
{literal ? (
|
||||
<td>
|
||||
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||
{value}
|
||||
</pre>
|
||||
</td>
|
||||
) : (
|
||||
<td>{value}</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function BankDetailsByPaytoType({
|
||||
payto,
|
||||
subject,
|
||||
exchangeBaseUrl,
|
||||
amount,
|
||||
}: BankDetailsProps): VNode {
|
||||
const firstPart = !payto.isKnown ? (
|
||||
<Fragment>
|
||||
<Row name="Account" value={payto.targetPath} />
|
||||
<Row name="Exchange" value={exchangeBaseUrl} />
|
||||
</Fragment>
|
||||
) : payto.targetType === "x-taler-bank" ? (
|
||||
<Fragment>
|
||||
<Row name="Bank host" value={payto.host} />
|
||||
<Row name="Bank account" value={payto.account} />
|
||||
<Row name="Exchange" value={exchangeBaseUrl} />
|
||||
</Fragment>
|
||||
) : payto.targetType === "iban" ? (
|
||||
<Fragment>
|
||||
<Row name="IBAN" value={payto.iban} />
|
||||
<Row name="Exchange" value={exchangeBaseUrl} />
|
||||
</Fragment>
|
||||
) : undefined;
|
||||
return (
|
||||
<table>
|
||||
{firstPart}
|
||||
<Row name="Amount" value={amount} />
|
||||
<Row name="Subject" value={subject} literal />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
export function ReserveCreated({
|
||||
reservePub,
|
||||
payto,
|
||||
@ -120,11 +24,12 @@ export function ReserveCreated({
|
||||
return <div>could not parse payto uri from exchange {payto}</div>;
|
||||
}
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h1>Bank transfer details</h1>
|
||||
<h1>Exchange is ready for withdrawal!</h1>
|
||||
<p>
|
||||
Please wire <b>{Amounts.stringify(amount)}</b> to:
|
||||
To complete the process you need to wire{" "}
|
||||
<b>{Amounts.stringify(amount)}</b> to the exchange bank account
|
||||
</p>
|
||||
<BankDetailsByPaytoType
|
||||
amount={Amounts.stringify(amount)}
|
||||
@ -132,14 +37,14 @@ export function ReserveCreated({
|
||||
payto={paytoURI}
|
||||
subject={reservePub}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
<WarningBox>
|
||||
Make sure to use the correct subject, otherwise the money will not
|
||||
arrive in this wallet.
|
||||
</WarningBox>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
Alternative, you can also scan this QR code or open{" "}
|
||||
<a href={payto}>this link</a> if you have a banking app installed that
|
||||
@ -149,8 +54,10 @@ export function ReserveCreated({
|
||||
</section>
|
||||
<footer>
|
||||
<div />
|
||||
<ButtonDestructive onClick={onBack}>Cancel withdraw</ButtonDestructive>
|
||||
<ButtonDestructive onClick={onBack}>
|
||||
Cancel withdrawal
|
||||
</ButtonDestructive>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -15,16 +15,15 @@
|
||||
*/
|
||||
|
||||
import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
|
||||
import { VNode, h, Fragment } from "preact";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { EditableText } from "../components/EditableText";
|
||||
import { SelectList } from "../components/SelectList";
|
||||
import { ButtonPrimary, ButtonSuccess, WalletBox } from "../components/styled";
|
||||
import { ButtonPrimary } from "../components/styled";
|
||||
import { useDevContext } from "../context/devContext";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
|
||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { useLang } from "../hooks/useLang";
|
||||
// import { strings as messages } from "../i18n/strings";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function SettingsPage(): VNode {
|
||||
@ -32,7 +31,7 @@ export function SettingsPage(): VNode {
|
||||
const { devMode, toggleDevMode } = useDevContext();
|
||||
const { name, update } = useBackupDeviceName();
|
||||
const [lang, changeLang] = useLang();
|
||||
const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
|
||||
const exchangesHook = useAsyncAsHook(wxApi.listExchanges);
|
||||
|
||||
return (
|
||||
<SettingsView
|
||||
@ -65,34 +64,32 @@ export interface ViewProps {
|
||||
knownExchanges: Array<ExchangeListItem>;
|
||||
}
|
||||
|
||||
import { strings as messages } from "../i18n/strings";
|
||||
// type LangsNames = {
|
||||
// [P in keyof typeof messages]: string;
|
||||
// };
|
||||
|
||||
type LangsNames = {
|
||||
[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]",
|
||||
};
|
||||
// const names: LangsNames = {
|
||||
// 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,
|
||||
// lang,
|
||||
// changeLang,
|
||||
// deviceName,
|
||||
// setDeviceName,
|
||||
permissionsEnabled,
|
||||
togglePermissions,
|
||||
developerMode,
|
||||
toggleDeveloperMode,
|
||||
}: ViewProps): VNode {
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section>
|
||||
<h2>
|
||||
<i18n.Translate>Known exchanges</i18n.Translate>
|
||||
@ -100,17 +97,23 @@ export function SettingsView({
|
||||
{!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>
|
||||
))}
|
||||
</table>
|
||||
<Fragment>
|
||||
<table>
|
||||
{knownExchanges.map((e, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>{e.currency}</td>
|
||||
<td>
|
||||
<a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Fragment>
|
||||
)}
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary>Manage exchange</ButtonPrimary>
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
<i18n.Translate>Permissions</i18n.Translate>
|
||||
@ -131,6 +134,6 @@ export function SettingsView({
|
||||
onToggle={toggleDeveloperMode}
|
||||
/>
|
||||
</section>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ const exampleData = {
|
||||
exchangeBaseUrl: "http://exchange.taler",
|
||||
withdrawalDetails: {
|
||||
confirmed: false,
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
|
||||
type: WithdrawalType.ManualTransfer,
|
||||
},
|
||||
@ -134,10 +135,49 @@ export const WithdrawError = createExample(TestedComponent, {
|
||||
},
|
||||
});
|
||||
|
||||
export const WithdrawPending = createExample(TestedComponent, {
|
||||
transaction: { ...exampleData.withdraw, pending: true },
|
||||
export const WithdrawPendingManual = createExample(TestedComponent, {
|
||||
transaction: {
|
||||
...exampleData.withdraw,
|
||||
withdrawalDetails: {
|
||||
type: WithdrawalType.ManualTransfer,
|
||||
exchangePaytoUris: ["payto://iban/asdasdasd"],
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
},
|
||||
pending: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const WithdrawPendingTalerBankUnconfirmed = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
transaction: {
|
||||
...exampleData.withdraw,
|
||||
withdrawalDetails: {
|
||||
type: WithdrawalType.TalerBankIntegrationApi,
|
||||
confirmed: false,
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
bankConfirmationUrl: "http://bank.demo.taler.net",
|
||||
},
|
||||
pending: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const WithdrawPendingTalerBankConfirmed = createExample(
|
||||
TestedComponent,
|
||||
{
|
||||
transaction: {
|
||||
...exampleData.withdraw,
|
||||
withdrawalDetails: {
|
||||
type: WithdrawalType.TalerBankIntegrationApi,
|
||||
confirmed: true,
|
||||
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
|
||||
},
|
||||
pending: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const Payment = createExample(TestedComponent, {
|
||||
transaction: exampleData.payment,
|
||||
});
|
||||
|
@ -18,62 +18,80 @@ import {
|
||||
AmountLike,
|
||||
Amounts,
|
||||
i18n,
|
||||
NotificationType,
|
||||
parsePaytoUri,
|
||||
Transaction,
|
||||
TransactionType,
|
||||
WithdrawalType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { ComponentChildren, Fragment, h, VNode } from "preact";
|
||||
import { route } from "preact-router";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useState } from "preact/hooks";
|
||||
import emptyImg from "../../static/img/empty.png";
|
||||
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType";
|
||||
import { ErrorMessage } from "../components/ErrorMessage";
|
||||
import { Part } from "../components/Part";
|
||||
import {
|
||||
Button,
|
||||
ButtonDestructive,
|
||||
ButtonPrimary,
|
||||
CenteredDialog,
|
||||
InfoBox,
|
||||
ListOfProducts,
|
||||
Overlay,
|
||||
RowBorderGray,
|
||||
SmallLightText,
|
||||
WalletBox,
|
||||
WarningBox,
|
||||
} from "../components/styled";
|
||||
import { Time } from "../components/Time";
|
||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { Pages } from "../NavigationBar";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
export function TransactionPage({ tid }: { tid: string }): VNode {
|
||||
const [transaction, setTransaction] = useState<Transaction | undefined>(
|
||||
undefined,
|
||||
);
|
||||
async function getTransaction(): Promise<Transaction> {
|
||||
const res = await wxApi.getTransactions();
|
||||
const ts = res.transactions.filter((t) => t.transactionId === tid);
|
||||
if (ts.length > 1) throw Error("more than one transaction with this id");
|
||||
if (ts.length === 1) {
|
||||
return ts[0];
|
||||
}
|
||||
throw Error("no transaction found");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async (): Promise<void> => {
|
||||
const res = await wxApi.getTransactions();
|
||||
const ts = res.transactions.filter((t) => t.transactionId === tid);
|
||||
if (ts.length === 1) {
|
||||
setTransaction(ts[0]);
|
||||
} else {
|
||||
route(Pages.history);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [tid]);
|
||||
const state = useAsyncAsHook(getTransaction, [
|
||||
NotificationType.WithdrawGroupFinished,
|
||||
]);
|
||||
|
||||
if (!transaction) {
|
||||
if (!state) {
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>Loading ...</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.hasError) {
|
||||
route(Pages.history);
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate>
|
||||
There was an error. Redirecting into the history page
|
||||
</i18n.Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function goToHistory(): void {
|
||||
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);
|
||||
}}
|
||||
transaction={state.response}
|
||||
onDelete={() => wxApi.deleteTransaction(tid).then(goToHistory)}
|
||||
onRetry={() => wxApi.retryTransaction(tid).then(goToHistory)}
|
||||
onBack={goToHistory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -91,16 +109,28 @@ export function TransactionView({
|
||||
onRetry,
|
||||
onBack,
|
||||
}: WalletTransactionProps): VNode {
|
||||
function TransactionTemplate({ children }: { children: VNode[] }): VNode {
|
||||
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
|
||||
function doCheckBeforeForget(): void {
|
||||
if (
|
||||
transaction.pending &&
|
||||
transaction.type === TransactionType.Withdrawal
|
||||
) {
|
||||
setConfirmBeforeForget(true);
|
||||
} else {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
function TransactionTemplate({
|
||||
children,
|
||||
}: {
|
||||
children: ComponentChildren;
|
||||
}): VNode {
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<section style={{ padding: 8, textAlign: "center" }}>
|
||||
<ErrorMessage title={transaction?.error?.hint} />
|
||||
{transaction.pending && (
|
||||
<WarningBox>
|
||||
This transaction is not completed
|
||||
<a href="">more info...</a>
|
||||
</WarningBox>
|
||||
<WarningBox>This transaction is not completed</WarningBox>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
@ -116,12 +146,12 @@ export function TransactionView({
|
||||
<i18n.Translate>retry</i18n.Translate>
|
||||
</ButtonPrimary>
|
||||
) : null}
|
||||
<ButtonDestructive onClick={onDelete}>
|
||||
<ButtonDestructive onClick={doCheckBeforeForget}>
|
||||
<i18n.Translate> Forget </i18n.Translate>
|
||||
</ButtonDestructive>
|
||||
</div>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -138,27 +168,119 @@ export function TransactionView({
|
||||
).amount;
|
||||
return (
|
||||
<TransactionTemplate>
|
||||
{confirmBeforeForget ? (
|
||||
<Overlay>
|
||||
<CenteredDialog>
|
||||
<header>Caution!</header>
|
||||
<section>
|
||||
If you have already wired money to the exchange you will loose
|
||||
the chance to get the coins form it.
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={() => setConfirmBeforeForget(false)}>
|
||||
<i18n.Translate> Cancel </i18n.Translate>
|
||||
</Button>
|
||||
|
||||
<ButtonDestructive onClick={onDelete}>
|
||||
<i18n.Translate> Confirm </i18n.Translate>
|
||||
</ButtonDestructive>
|
||||
</footer>
|
||||
</CenteredDialog>
|
||||
</Overlay>
|
||||
) : undefined}
|
||||
<h2>Withdrawal</h2>
|
||||
<Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
|
||||
<br />
|
||||
<Part
|
||||
big
|
||||
title="Total withdrawn"
|
||||
text={amountToString(transaction.amountEffective)}
|
||||
kind="positive"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Chosen amount"
|
||||
text={amountToString(transaction.amountRaw)}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Exchange fee"
|
||||
text={amountToString(fee)}
|
||||
kind="negative"
|
||||
/>
|
||||
{transaction.pending ? (
|
||||
transaction.withdrawalDetails.type ===
|
||||
WithdrawalType.ManualTransfer ? (
|
||||
<Fragment>
|
||||
<BankDetailsByPaytoType
|
||||
amount={amountToString(transaction.amountRaw)}
|
||||
exchangeBaseUrl={transaction.exchangeBaseUrl}
|
||||
payto={parsePaytoUri(
|
||||
transaction.withdrawalDetails.exchangePaytoUris[0],
|
||||
)}
|
||||
subject={transaction.withdrawalDetails.reservePub}
|
||||
/>
|
||||
<p>
|
||||
<WarningBox>
|
||||
Make sure to use the correct subject, otherwise the money will
|
||||
not arrive in this wallet.
|
||||
</WarningBox>
|
||||
</p>
|
||||
<Part
|
||||
big
|
||||
title="Total withdrawn"
|
||||
text={amountToString(transaction.amountEffective)}
|
||||
kind="positive"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Exchange fee"
|
||||
text={amountToString(fee)}
|
||||
kind="negative"
|
||||
/>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{!transaction.withdrawalDetails.confirmed &&
|
||||
transaction.withdrawalDetails.bankConfirmationUrl ? (
|
||||
<InfoBox>
|
||||
The bank is waiting for confirmation. Go to the
|
||||
<a
|
||||
href={transaction.withdrawalDetails.bankConfirmationUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
bank site
|
||||
</a>
|
||||
</InfoBox>
|
||||
) : undefined}
|
||||
{transaction.withdrawalDetails.confirmed && (
|
||||
<InfoBox>Waiting for the coins to arrive</InfoBox>
|
||||
)}
|
||||
<Part
|
||||
big
|
||||
title="Total withdrawn"
|
||||
text={amountToString(transaction.amountEffective)}
|
||||
kind="positive"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Chosen amount"
|
||||
text={amountToString(transaction.amountRaw)}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Exchange fee"
|
||||
text={amountToString(fee)}
|
||||
kind="negative"
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
) : (
|
||||
<Fragment>
|
||||
<Part
|
||||
big
|
||||
title="Total withdrawn"
|
||||
text={amountToString(transaction.amountEffective)}
|
||||
kind="positive"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Chosen amount"
|
||||
text={amountToString(transaction.amountRaw)}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Exchange fee"
|
||||
text={amountToString(fee)}
|
||||
kind="negative"
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Part
|
||||
title="Exchange"
|
||||
text={new URL(transaction.exchangeBaseUrl).hostname}
|
||||
|
@ -20,13 +20,12 @@
|
||||
* @author Florian Dold
|
||||
*/
|
||||
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||
import { Diagnostics } from "../components/Diagnostics";
|
||||
import { WalletBox } from "../components/styled";
|
||||
import { useDiagnostics } from "../hooks/useDiagnostics";
|
||||
import { WalletDiagnostics } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { Diagnostics } from "../components/Diagnostics";
|
||||
import { useDiagnostics } from "../hooks/useDiagnostics";
|
||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||
|
||||
export function WelcomePage(): VNode {
|
||||
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
|
||||
@ -54,7 +53,7 @@ export function View({
|
||||
timedOut,
|
||||
}: ViewProps): VNode {
|
||||
return (
|
||||
<WalletBox>
|
||||
<Fragment>
|
||||
<h1>Browser Extension Installed!</h1>
|
||||
<div>
|
||||
<p>Thank you for installing the wallet.</p>
|
||||
@ -75,6 +74,6 @@ export function View({
|
||||
Learn how to top up your wallet balance »
|
||||
</a>
|
||||
</div>
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
import { setupI18n } from "@gnu-taler/taler-util";
|
||||
import { createHashHistory } from "history";
|
||||
import { Fragment, h, render } from "preact";
|
||||
import { Fragment, h, render, VNode } from "preact";
|
||||
import Router, { route, Route } from "preact-router";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { LogoHeader } from "./components/LogoHeader";
|
||||
@ -39,8 +39,11 @@ import { SettingsPage } from "./wallet/Settings";
|
||||
import { TransactionPage } from "./wallet/Transaction";
|
||||
import { WelcomePage } from "./wallet/Welcome";
|
||||
import { BackupPage } from "./wallet/BackupPage";
|
||||
import { DeveloperPage } from "./popup/Debug.js";
|
||||
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js";
|
||||
import { DeveloperPage } from "./popup/Debug";
|
||||
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage";
|
||||
import { WalletBox } from "./components/styled";
|
||||
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
|
||||
import { ProviderAddPage } from "./wallet/ProviderAddPage";
|
||||
|
||||
function main(): void {
|
||||
try {
|
||||
@ -66,16 +69,20 @@ if (document.readyState === "loading") {
|
||||
}
|
||||
|
||||
function withLogoAndNavBar(Component: any) {
|
||||
return (props: any) => (
|
||||
<Fragment>
|
||||
<LogoHeader />
|
||||
<WalletNavBar />
|
||||
<Component {...props} />
|
||||
</Fragment>
|
||||
);
|
||||
return function withLogoAndNavBarComponent(props: any): VNode {
|
||||
return (
|
||||
<Fragment>
|
||||
<LogoHeader />
|
||||
<WalletNavBar />
|
||||
<WalletBox>
|
||||
<Component {...props} />
|
||||
</WalletBox>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function Application() {
|
||||
function Application(): VNode {
|
||||
return (
|
||||
<div>
|
||||
<DevContextProvider>
|
||||
@ -105,6 +112,23 @@ function Application() {
|
||||
<Route
|
||||
path={Pages.backup}
|
||||
component={withLogoAndNavBar(BackupPage)}
|
||||
onAddProvider={() => {
|
||||
route(Pages.provider_add);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={Pages.provider_detail}
|
||||
component={withLogoAndNavBar(ProviderDetailPage)}
|
||||
onBack={() => {
|
||||
route(Pages.backup);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={Pages.provider_add}
|
||||
component={withLogoAndNavBar(ProviderAddPage)}
|
||||
onBack={() => {
|
||||
route(Pages.backup);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
@ -22,39 +22,21 @@
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
CoreApiResponse,
|
||||
ConfirmPayResult,
|
||||
BalancesResponse,
|
||||
TransactionsResponse,
|
||||
ApplyRefundResponse,
|
||||
PreparePayResult,
|
||||
AcceptWithdrawalResponse,
|
||||
WalletDiagnostics,
|
||||
GetWithdrawalDetailsForUriRequest,
|
||||
WithdrawUriInfoResponse,
|
||||
PrepareTipRequest,
|
||||
PrepareTipResult,
|
||||
AcceptTipRequest,
|
||||
DeleteTransactionRequest,
|
||||
RetryTransactionRequest,
|
||||
SetWalletDeviceIdRequest,
|
||||
GetExchangeWithdrawalInfo,
|
||||
AcceptExchangeTosRequest,
|
||||
AcceptManualWithdrawalResult,
|
||||
AcceptManualWithdrawalRequest,
|
||||
AmountJson,
|
||||
ExchangesListRespose,
|
||||
AddExchangeRequest,
|
||||
GetExchangeTosResult,
|
||||
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
|
||||
AddExchangeRequest, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
|
||||
CoreApiResponse, DeleteTransactionRequest, ExchangesListRespose,
|
||||
GetExchangeTosResult, GetExchangeWithdrawalInfo,
|
||||
GetWithdrawalDetailsForUriRequest, NotificationType, PreparePayResult, PrepareTipRequest,
|
||||
PrepareTipResult, RetryTransactionRequest,
|
||||
SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
AddBackupProviderRequest,
|
||||
BackupProviderState,
|
||||
OperationFailedError,
|
||||
RemoveBackupProviderRequest,
|
||||
AddBackupProviderRequest, BackupInfo, 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";
|
||||
import { MessageFromBackend } from "./wxBackend.js";
|
||||
|
||||
export interface ExtendedPermissionsResponse {
|
||||
newValue: boolean;
|
||||
@ -83,7 +65,9 @@ export interface UpgradeResponse {
|
||||
|
||||
async function callBackend(operation: string, payload: any): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Error calling backend");
|
||||
reject(
|
||||
@ -366,10 +350,13 @@ export function acceptTip(req: AcceptTipRequest): Promise<void> {
|
||||
return callBackend("acceptTip", req);
|
||||
}
|
||||
|
||||
export function onUpdateNotification(f: () => void): () => void {
|
||||
export function onUpdateNotification(messageType: Array<NotificationType>, doCallback: () => void): () => void {
|
||||
// eslint-disable-next-line no-undef
|
||||
const port = chrome.runtime.connect({ name: "notifications" });
|
||||
const listener = (): void => {
|
||||
f();
|
||||
const listener = (message: MessageFromBackend): void => {
|
||||
if (messageType.includes(message.type)) {
|
||||
doCallback();
|
||||
}
|
||||
};
|
||||
port.onMessage.addListener(listener);
|
||||
return () => {
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
classifyTalerUri,
|
||||
CoreApiResponse,
|
||||
CoreApiResponseSuccess,
|
||||
NotificationType,
|
||||
TalerErrorCode,
|
||||
TalerUriType,
|
||||
WalletDiagnostics,
|
||||
@ -237,6 +238,10 @@ function makeSyncWalletRedirect(
|
||||
return { redirectUrl: innerUrl.href };
|
||||
}
|
||||
|
||||
export type MessageFromBackend = {
|
||||
type: NotificationType
|
||||
}
|
||||
|
||||
async function reinitWallet(): Promise<void> {
|
||||
if (currentWallet) {
|
||||
currentWallet.stop();
|
||||
@ -266,9 +271,10 @@ async function reinitWallet(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
wallet.addNotificationListener((x) => {
|
||||
for (const x of notificationPorts) {
|
||||
for (const notif of notificationPorts) {
|
||||
const message: MessageFromBackend = { type: x.type };
|
||||
try {
|
||||
x.postMessage({ type: "notification" });
|
||||
notif.postMessage(message);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user