copy from popup to wallet
This commit is contained in:
parent
147da7c160
commit
0bc235c64b
@ -23,7 +23,7 @@ import { createExample, NullLink } from '../test-utils';
|
||||
import { BalanceView as TestedComponent } from './BalancePage';
|
||||
|
||||
export default {
|
||||
title: 'popup/balance/detail',
|
||||
title: 'popup/balance',
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ import {
|
||||
import { ProviderAddPage } from "./popup/ProviderAddPage";
|
||||
import { ProviderDetailPage } from "./popup/ProviderDetailPage";
|
||||
import { SettingsPage } from "./popup/Settings";
|
||||
import { TransactionPage } from "./popup/Transaction";
|
||||
|
||||
function main(): void {
|
||||
try {
|
||||
@ -114,7 +113,6 @@ function Application() {
|
||||
route(Pages.backup)
|
||||
}}
|
||||
/>
|
||||
<Route path={Pages.transaction} component={TransactionPage} />
|
||||
<Route default component={Redirect} to={Pages.balance} />
|
||||
</Router>
|
||||
</div>
|
||||
|
193
packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
Normal file
193
packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
Normal 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: []
|
||||
});
|
||||
|
146
packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
Normal file
146
packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
Normal 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}`
|
||||
}
|
@ -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,
|
||||
});
|
117
packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
Normal file
117
packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
Normal 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>
|
||||
}
|
@ -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'
|
||||
}
|
||||
});
|
@ -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> < 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> < Back</i18n.Translate></Button>
|
||||
<ButtonPrimary
|
||||
disabled={!accepted}
|
||||
onClick={onConfirm}><i18n.Translate>Add provider</i18n.Translate></ButtonPrimary>
|
||||
</footer>
|
||||
</WalletBox>
|
||||
}
|
@ -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'
|
||||
});
|
@ -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"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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> -></td>
|
||||
<td><i18n.Translate>new</i18n.Translate></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td><i18n.Translate>fee</i18n.Translate></td>
|
||||
<td>{info.paymentStatus.oldTerms.annualFee}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i18n.Translate>storage</i18n.Translate></td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>}
|
||||
|
||||
</section>
|
||||
<footer>
|
||||
<Button onClick={onBack}><i18n.Translate> < back</i18n.Translate></Button>
|
||||
<div>
|
||||
<ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
|
||||
</div>
|
||||
</footer>
|
||||
</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 ''
|
||||
}
|
||||
}
|
@ -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(),
|
||||
});
|
||||
|
103
packages/taler-wallet-webextension/src/wallet/Settings.tsx
Normal file
103
packages/taler-wallet-webextension/src/wallet/Settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -30,7 +30,7 @@ import { createExample } from '../test-utils';
|
||||
import { TransactionView as TestedComponent } from './Transaction';
|
||||
|
||||
export default {
|
||||
title: 'popup/history/details',
|
||||
title: 'wallet/history/details',
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onRetry: { action: 'onRetry' },
|
@ -22,7 +22,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import * as wxApi from "../wxApi";
|
||||
import { Pages } from "../NavigationBar";
|
||||
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";
|
||||
|
||||
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[] }) {
|
||||
return <PopupBox>
|
||||
return <WalletBox>
|
||||
<header>
|
||||
<SmallTextLight>
|
||||
{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>
|
||||
</div>
|
||||
</footer>
|
||||
</PopupBox>
|
||||
</WalletBox>
|
||||
}
|
||||
|
||||
if (transaction.type === TransactionType.Withdrawal) {
|
@ -20,24 +20,28 @@
|
||||
* @author Florian Dold <dold@taler.net>
|
||||
*/
|
||||
|
||||
import { Fragment, render, h } from "preact";
|
||||
import { setupI18n } from "@gnu-taler/taler-util";
|
||||
import { strings } from "./i18n/strings";
|
||||
import { createHashHistory } from 'history';
|
||||
|
||||
import { WelcomePage } from "./wallet/Welcome";
|
||||
import { HistoryPage } from "./wallet/History";
|
||||
import { WithdrawPage } from "./cta/Withdraw";
|
||||
import { Fragment, h, render } from "preact";
|
||||
import Router, { route, Route } from "preact-router";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { LogoHeader } from "./components/LogoHeader";
|
||||
import { DevContextProvider } from "./context/devContext";
|
||||
import { PayPage } from "./cta/Pay";
|
||||
import { RefundPage } from "./cta/Refund";
|
||||
import { TipPage } from './cta/Tip';
|
||||
import Router, { route, Route } from "preact-router";
|
||||
import { DevContextProvider } from "./context/devContext";
|
||||
import { LogoHeader } from "./components/LogoHeader";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { WithdrawPage } from "./cta/Withdraw";
|
||||
import { strings } from "./i18n/strings";
|
||||
import {
|
||||
Pages, WalletNavBar
|
||||
} from "./NavigationBar";
|
||||
import { BalancePage } from "./wallet/BalancePage";
|
||||
import { HistoryPage } from "./wallet/History";
|
||||
import { SettingsPage } from "./wallet/Settings";
|
||||
import { TransactionPage } from './wallet/Transaction';
|
||||
import { WelcomePage } from "./wallet/Welcome";
|
||||
import { BackupPage } from './wallet/BackupPage';
|
||||
|
||||
|
||||
function main(): void {
|
||||
try {
|
||||
@ -76,7 +80,10 @@ function Application() {
|
||||
<Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} />
|
||||
|
||||
<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.payback} component={() => <div>no yet implemented</div>} />
|
||||
|
Loading…
Reference in New Issue
Block a user