copy from popup to wallet

This commit is contained in:
Sebastian 2021-08-24 13:29:37 -03:00
parent 147da7c160
commit 0bc235c64b
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 1420 additions and 18 deletions

View File

@ -23,7 +23,7 @@ import { createExample, NullLink } from '../test-utils';
import { BalanceView as TestedComponent } from './BalancePage'; import { BalanceView as TestedComponent } from './BalancePage';
export default { export default {
title: 'popup/balance/detail', title: 'popup/balance',
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
} }

View File

@ -38,7 +38,6 @@ import {
import { ProviderAddPage } from "./popup/ProviderAddPage"; import { ProviderAddPage } from "./popup/ProviderAddPage";
import { ProviderDetailPage } from "./popup/ProviderDetailPage"; import { ProviderDetailPage } from "./popup/ProviderDetailPage";
import { SettingsPage } from "./popup/Settings"; import { SettingsPage } from "./popup/Settings";
import { TransactionPage } from "./popup/Transaction";
function main(): void { function main(): void {
try { try {
@ -114,7 +113,6 @@ function Application() {
route(Pages.backup) route(Pages.backup)
}} }}
/> />
<Route path={Pages.transaction} component={TransactionPage} />
<Route default component={Redirect} to={Pages.balance} /> <Route default component={Redirect} to={Pages.balance} />
</Router> </Router>
</div> </div>

View File

@ -0,0 +1,193 @@
/*
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: 'wallet/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

@ -0,0 +1,146 @@
/*
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, JSX, VNode, h } from "preact";
import {
BoldLight, ButtonPrimary, ButtonSuccess, Centered,
CenteredText, CenteredTextBold, PopupBox, RowBorderGray,
SmallText, SmallTextLight, WalletBox
} 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 (
<WalletBox>
<section>
{providers.map((provider) => <BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={provider.syncProviderBaseUrl}
active={provider.active}
title={provider.name}
/>
)}
{!providers.length && <Centered style={{marginTop: 100}}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
</Centered>}
</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>}
</WalletBox>
)
}
interface TransactionLayoutProps {
status: ProviderPaymentStatus;
timestamp?: Timestamp;
title: string;
id: string;
active: boolean;
}
function BackupLayout(props: TransactionLayoutProps): JSX.Element {
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 && <SmallTextLight style={{marginTop: 5}}>Not synced</SmallTextLight>}
</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>
<CenteredTextBold {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredTextBold>
</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

@ -0,0 +1,105 @@
/*
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, NullLink } from '../test-utils';
import { BalanceView as TestedComponent } from './BalancePage';
export default {
title: 'wallet/balance',
component: TestedComponent,
argTypes: {
}
};
export const NotYetLoaded = createExample(TestedComponent, {
});
export const GotError = createExample(TestedComponent, {
balance: {
error: true
},
Linker: NullLink,
});
export const EmptyBalance = createExample(TestedComponent, {
balance: {
error: false,
response: {
balances: []
},
},
Linker: NullLink,
});
export const SomeCoins = createExample(TestedComponent, {
balance: {
error: false,
response: {
balances: [{
available: 'USD:10.5',
hasPendingTransactions: false,
pendingIncoming: 'USD:0',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
},
},
Linker: NullLink,
});
export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
balance: {
error: false,
response: {
balances: [{
available: 'USD:2.23',
hasPendingTransactions: false,
pendingIncoming: 'USD:5.11',
pendingOutgoing: 'USD:0',
requiresUserInput: false
}]
},
},
Linker: NullLink,
});
export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
balance: {
error: false,
response: {
balances: [{
available: 'USD:2',
hasPendingTransactions: false,
pendingIncoming: 'USD:5',
pendingOutgoing: 'USD:0',
requiresUserInput: false
},{
available: 'EUR:4',
hasPendingTransactions: false,
pendingIncoming: 'EUR:5',
pendingOutgoing: 'EUR:0',
requiresUserInput: false
}]
},
},
Linker: NullLink,
});

View File

@ -0,0 +1,117 @@
/*
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, BalancesResponse,
i18n
} from "@gnu-taler/taler-util";
import { JSX, h } from "preact";
import { WalletBox, Centered } from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
export function BalancePage() {
const balance = useBalances()
return <BalanceView balance={balance} Linker={PageLink} />
}
export interface BalanceViewProps {
balance: BalancesHook,
Linker: typeof PageLink,
}
export function BalanceView({ balance, Linker }: BalanceViewProps) {
if (!balance) {
return <span />
}
if (balance.error) {
return (
<div>
<p>{i18n.str`Error: could not retrieve balance information.`}</p>
<p>
Click <Linker pageName="welcome">here</Linker> for help and
diagnostics.
</p>
</div>
)
}
if (balance.response.balances.length === 0) {
return (
<p><i18n.Translate>
You have no balance to show. Need some{" "}
<Linker pageName="/welcome">help</Linker> getting started?
</i18n.Translate></p>
)
}
return <ShowBalances wallet={balance.response} />
}
function formatPending(entry: Balance): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | 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 }: { wallet: BalancesResponse }) {
return <WalletBox>
<section>
<Centered>{wallet.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
const v = av.value + av.fraction / amountFractionalBase;
return (
<p key={av.currency}>
<span>
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
<span>{av.currency}</span>
</span>
{formatPending(entry)}
</p>
);
})}</Centered>
</section>
</WalletBox>
}

View File

@ -0,0 +1,52 @@
/*
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: 'wallet/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

@ -0,0 +1,150 @@
import { Amounts, BackupBackupProviderTerms, canonicalizeBaseUrl, i18n } from "@gnu-taler/taler-util";
import { verify } from "@gnu-taler/taler-wallet-core/src/crypto/primitives/nacl-fast";
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, WalletBox, SmallTextLight } 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 <WalletBox>
<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>
</WalletBox>
}
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 <WalletBox>
<section>
<h1>Review terms of service</h1>
<div>Provider URL: <a href={url} target="_blank">{url}</a></div>
<SmallTextLight>Please review and accept this provider's terms of service</SmallTextLight>
<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>
</WalletBox>
}

View File

@ -0,0 +1,53 @@
/*
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: 'wallet/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

@ -0,0 +1,238 @@
/*
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: 'wallet/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

@ -0,0 +1,197 @@
/*
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, WalletBox, SmallTextLight } 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 (
<WalletBox>
{info.backupProblem || info.lastError ? <header>
<Error info={info} />
</header> : undefined }
<header>
<h3>{info.name} <SmallTextLight>{info.syncProviderBaseUrl}</SmallTextLight></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>
</WalletBox>
)
}
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

@ -0,0 +1,43 @@
/*
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 { SettingsView as TestedComponent } from './Settings';
export default {
title: 'wallet/settings',
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
}
};
export const AllOff = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
setDeviceName: () => Promise.resolve(),
});
export const OneChecked = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
});

View File

@ -0,0 +1,103 @@
/*
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 } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { Checkbox } from "../components/Checkbox";
import { EditableText } from "../components/EditableText";
import { SelectList } from "../components/SelectList";
import { useDevContext } from "../context/devContext";
import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
import { useLang } from "../hooks/useLang";
export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const { devMode, toggleDevMode } = useDevContext()
const { name, update } = useBackupDeviceName()
const [lang, changeLang] = useLang()
return <SettingsView
lang={lang} changeLang={changeLang}
deviceName={name} setDeviceName={update}
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
developerMode={devMode} toggleDeveloperMode={toggleDevMode}
/>;
}
export interface ViewProps {
lang: string;
changeLang: (s: string) => void;
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
permissionsEnabled: boolean;
togglePermissions: () => void;
developerMode: boolean;
toggleDeveloperMode: () => void;
}
import { strings as messages } from '../i18n/strings'
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]',
}
export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
return (
<div>
<section style={{ height: 300, overflow: 'auto' }}>
<h2><i18n.Translate>Wallet</i18n.Translate></h2>
<SelectList
value={lang}
onChange={changeLang}
name="lang"
list={names}
label={i18n.str`Language`}
description="(Choose your preferred lang)"
/>
<EditableText
value={deviceName}
onChange={setDeviceName}
name="device-id"
label={i18n.str`Device name`}
description="(This is how you will recognize the wallet in the backup provider)"
/>
<h2><i18n.Translate>Permissions</i18n.Translate></h2>
<Checkbox label="Automatically open wallet based on page content"
name="perm"
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
enabled={permissionsEnabled} onToggle={togglePermissions}
/>
<h2>Config</h2>
<Checkbox label="Developer mode"
name="devMode"
description="(More options and information useful for debugging)"
enabled={developerMode} onToggle={toggleDeveloperMode}
/>
</section>
</div>
)
}

View File

@ -30,7 +30,7 @@ import { createExample } from '../test-utils';
import { TransactionView as TestedComponent } from './Transaction'; import { TransactionView as TestedComponent } from './Transaction';
export default { export default {
title: 'popup/history/details', title: 'wallet/history/details',
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: 'onRetry' }, onRetry: { action: 'onRetry' },

View File

@ -22,7 +22,7 @@ import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
import { Pages } from "../NavigationBar"; import { Pages } from "../NavigationBar";
import emptyImg from "../../static/img/empty.png" import emptyImg from "../../static/img/empty.png"
import { Button, ButtonDestructive, ButtonPrimary, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight } from "../components/styled"; import { Button, ButtonDestructive, ButtonPrimary, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight, WalletBox } from "../components/styled";
import { ErrorMessage } from "../components/ErrorMessage"; import { ErrorMessage } from "../components/ErrorMessage";
export function TransactionPage({ tid }: { tid: string; }): JSX.Element { export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
@ -79,7 +79,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall
} }
function TransactionTemplate({ upperRight, children }: { upperRight: VNode, children: VNode[] }) { function TransactionTemplate({ upperRight, children }: { upperRight: VNode, children: VNode[] }) {
return <PopupBox> return <WalletBox>
<header> <header>
<SmallTextLight> <SmallTextLight>
{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')} {transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}
@ -99,7 +99,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall
<ButtonDestructive onClick={onDelete}><i18n.Translate>delete</i18n.Translate></ButtonDestructive> <ButtonDestructive onClick={onDelete}><i18n.Translate>delete</i18n.Translate></ButtonDestructive>
</div> </div>
</footer> </footer>
</PopupBox> </WalletBox>
} }
if (transaction.type === TransactionType.Withdrawal) { if (transaction.type === TransactionType.Withdrawal) {

View File

@ -20,24 +20,28 @@
* @author Florian Dold <dold@taler.net> * @author Florian Dold <dold@taler.net>
*/ */
import { Fragment, render, h } from "preact";
import { setupI18n } from "@gnu-taler/taler-util"; import { setupI18n } from "@gnu-taler/taler-util";
import { strings } from "./i18n/strings";
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import { Fragment, h, render } from "preact";
import { WelcomePage } from "./wallet/Welcome"; import Router, { route, Route } from "preact-router";
import { HistoryPage } from "./wallet/History"; import { useEffect } from "preact/hooks";
import { WithdrawPage } from "./cta/Withdraw"; import { LogoHeader } from "./components/LogoHeader";
import { DevContextProvider } from "./context/devContext";
import { PayPage } from "./cta/Pay"; import { PayPage } from "./cta/Pay";
import { RefundPage } from "./cta/Refund"; import { RefundPage } from "./cta/Refund";
import { TipPage } from './cta/Tip'; import { TipPage } from './cta/Tip';
import Router, { route, Route } from "preact-router"; import { WithdrawPage } from "./cta/Withdraw";
import { DevContextProvider } from "./context/devContext"; import { strings } from "./i18n/strings";
import { LogoHeader } from "./components/LogoHeader";
import { useEffect } from "preact/hooks";
import { import {
Pages, WalletNavBar Pages, WalletNavBar
} from "./NavigationBar"; } from "./NavigationBar";
import { BalancePage } from "./wallet/BalancePage";
import { HistoryPage } from "./wallet/History";
import { SettingsPage } from "./wallet/Settings";
import { TransactionPage } from './wallet/Transaction';
import { WelcomePage } from "./wallet/Welcome";
import { BackupPage } from './wallet/BackupPage';
function main(): void { function main(): void {
try { try {
@ -76,7 +80,10 @@ function Application() {
<Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} /> <Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} />
<Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} /> <Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} />
<Route path={Pages.transaction} component={withLogoAndNavBar(HistoryPage)} /> <Route path={Pages.transaction} component={withLogoAndNavBar(TransactionPage)} />
<Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)} />
<Route path={Pages.settings} component={withLogoAndNavBar(SettingsPage)} />
<Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} />
<Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} /> <Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} />
<Route path={Pages.payback} component={() => <div>no yet implemented</div>} /> <Route path={Pages.payback} component={() => <div>no yet implemented</div>} />