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:
Sebastian 2021-11-19 14:51:27 -03:00
parent 60cfb0e78f
commit a35604fd56
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
40 changed files with 956 additions and 1964 deletions

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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 || [];

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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: [],
});

View File

@ -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}`;
}

View File

@ -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",

View File

@ -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>
);
}

View File

@ -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 {

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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",
},
});

View File

@ -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> &lt; 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> &lt; Back</i18n.Translate>
</Button>
<ButtonPrimary disabled={!accepted} onClick={onConfirm}>
<i18n.Translate>Add provider</i18n.Translate>
</ButtonPrimary>
</footer>
</PopupBox>
);
}

View File

@ -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",
});

View File

@ -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",
},
},
});

View File

@ -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> -&gt;</td>
<td>
<i18n.Translate>new</i18n.Translate>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<i18n.Translate>fee</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td>
<i18n.Translate>storage</i18n.Translate>
</td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>
)}
</section>
<footer>
<Button onClick={onBack}>
<i18n.Translate> &lt; back</i18n.Translate>
</Button>
<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 "";
}
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View 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>
);

View File

@ -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(),

View File

@ -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>
);
}

View File

@ -34,6 +34,10 @@ const exchangeList = {
"http://exchange.tal": "EUR",
};
export const WithoutAnyExchangeKnown = createExample(TestedComponent, {
exchangeList: {},
});
export const InitialState = createExample(TestedComponent, {
exchangeList,
});

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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`

View File

@ -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}>
&nbsp; Copy &nbsp;
</ButtonPrimary>
) : (
<ButtonPrimary small disabled>
Copied
</ButtonPrimary>
)}
</td>
<td>
<b>{name}</b>
</td>
{literal ? (
<td>
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
{value}
</pre>
</td>
) : (
<td>{value}</td>
)}
</tr>
);
}
function BankDetailsByPaytoType({
payto,
subject,
exchangeBaseUrl,
amount,
}: BankDetailsProps): VNode {
const firstPart = !payto.isKnown ? (
<Fragment>
<Row name="Account" value={payto.targetPath} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : payto.targetType === "x-taler-bank" ? (
<Fragment>
<Row name="Bank host" value={payto.host} />
<Row name="Bank account" value={payto.account} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : payto.targetType === "iban" ? (
<Fragment>
<Row name="IBAN" value={payto.iban} />
<Row name="Exchange" value={exchangeBaseUrl} />
</Fragment>
) : undefined;
return (
<table>
{firstPart}
<Row name="Amount" value={amount} />
<Row name="Subject" value={subject} literal />
</table>
);
}
export function ReserveCreated({
reservePub,
payto,
@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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,
});

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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

View File

@ -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 () => {

View File

@ -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);
}