summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-24 13:29:37 -0300
committerSebastian <sebasjm@gmail.com>2021-08-24 15:16:33 -0300
commit0bc235c64b6936aa092a2df40e0c4909e4ac05d5 (patch)
treec5dde44f6bab19365a4ba4ba0aab46febf2cad0c /packages/taler-wallet-webextension
parent147da7c160c0175c2722f4587ccb2916a3f056d9 (diff)
downloadwallet-core-0bc235c64b6936aa092a2df40e0c4909e4ac05d5.tar.gz
wallet-core-0bc235c64b6936aa092a2df40e0c4909e4ac05d5.tar.bz2
wallet-core-0bc235c64b6936aa092a2df40e0c4909e4ac05d5.zip
copy from popup to wallet
Diffstat (limited to 'packages/taler-wallet-webextension')
-rw-r--r--packages/taler-wallet-webextension/src/popup/Balance.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx193
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx146
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx105
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BalancePage.tsx117
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx52
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx150
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx238
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx197
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx43
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx103
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx (renamed from packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx)2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx (renamed from packages/taler-wallet-webextension/src/popup/Transaction.tsx)6
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.tsx29
16 files changed, 1420 insertions, 18 deletions
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 4a2e1045b..a0655d379 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -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: {
}
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index 77c19c150..c72ea85c4 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -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>
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
new file mode 100644
index 000000000..9a53fefe2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -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: []
+});
+
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
new file mode 100644
index 000000000..8b88432e0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -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}`
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
new file mode 100644
index 000000000..1b145345f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
@@ -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,
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
new file mode 100644
index 000000000..4846d47f7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
@@ -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>
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
new file mode 100644
index 000000000..d1e76c053
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
@@ -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'
+ }
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
new file mode 100644
index 000000000..2b205ebee
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
@@ -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>
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
new file mode 100644
index 000000000..4890e5e9c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
@@ -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'
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
new file mode 100644
index 000000000..67ff83442
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -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"
+ }
+ }
+});
+
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
new file mode 100644
index 000000000..fc361f625
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -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 ''
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
new file mode 100644
index 000000000..deb30e55f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -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(),
+});
+
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
new file mode 100644
index 000000000..52e72ee2f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -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>
+ )
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index 65302babb..0f7ea457d 100644
--- a/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -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' },
diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 117a098bc..d00abc16a 100644
--- a/packages/taler-wallet-webextension/src/popup/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -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) {
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
index 61df45e7e..aa007786c 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
@@ -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>} />