refactored backup sync UI

This commit is contained in:
Sebastian 2021-07-06 12:44:25 -03:00
parent 550905f0e7
commit 678a90934c
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 583 additions and 207 deletions

View File

@ -0,0 +1,69 @@
/*
This file is part of GNU Taler
(C) 2019 Taler Systems SA
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { VNode } from "preact";
import { useRef, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
interface Props {
value: string;
onChange: (s: string) => Promise<void>;
label: string;
name: string;
description?: string;
}
export function EditableText({ name, value, onChange, label, description }: Props): JSX.Element {
const [editing, setEditing] = useState(false)
const ref = useRef<HTMLInputElement>()
let InputText;
if (!editing) {
InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<p>{value}</p>
<button onClick={() => setEditing(true)}>edit</button>
</div>
} else {
InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<input
value={value}
ref={ref}
type="text"
id={`text-${name}`}
/>
<button onClick={() => { onChange(ref.current.value).then(r => setEditing(false)) }}>confirm</button>
</div>
}
return (
<div>
<label
htmlFor={`text-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
>
{label}
</label>
<InputText />
{description && <span
style={{
color: "#383838",
fontSize: "smaller",
display: "block",
marginLeft: "2em",
}}
>
{description}
</span>}
</div>
);
}

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
export interface BackupDeviceName {
name: string;
update: (s:string) => Promise<void>
}
export function useBackupDeviceName(): BackupDeviceName {
const [status, setStatus] = useState<BackupDeviceName>({
name: '',
update: () => Promise.resolve()
})
useEffect(() => {
async function run() {
//create a first list of backup info by currency
const status = await wxApi.getBackupInfo()
async function update(newName: string) {
await wxApi.setWalletDeviceId(newName)
setStatus(old => ({ ...old, name: newName }))
}
setStatus({ name: status.deviceId, update })
}
run()
}, [])
return status
}

View File

@ -1,37 +1,43 @@
import { Amounts } from "@gnu-taler/taler-util"; import { Amounts } from "@gnu-taler/taler-util";
import { ProviderInfo } from "@gnu-taler/taler-wallet-core"; import { ProviderInfo, ProviderPaymentPaid, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
export interface ProvidersByCurrency {
[s: string]: ProviderInfo | undefined
}
export interface BackupStatus { export interface BackupStatus {
deviceName: string; deviceName: string;
providers: ProvidersByCurrency providers: ProviderInfo[]
}
function getStatusTypeOrder(t: ProviderPaymentStatus) {
return [
ProviderPaymentType.InsufficientBalance,
ProviderPaymentType.TermsChanged,
ProviderPaymentType.Unpaid,
ProviderPaymentType.Paid,
ProviderPaymentType.Pending,
].indexOf(t.type)
}
function getStatusPaidOrder(a: ProviderPaymentPaid, b: ProviderPaymentPaid) {
return a.paidUntil.t_ms === 'never' ? -1 :
b.paidUntil.t_ms === 'never' ? 1 :
a.paidUntil.t_ms - b.paidUntil.t_ms
} }
export function useBackupStatus(): BackupStatus | undefined { export function useBackupStatus(): BackupStatus | undefined {
const [status, setStatus] = useState<BackupStatus | undefined>(undefined) const [status, setStatus] = useState<BackupStatus | undefined>(undefined)
useEffect(() => { useEffect(() => {
async function run() { async function run() {
//create a first list of backup info by currency //create a first list of backup info by currency
const status = await wxApi.getBackupInfo() const status = await wxApi.getBackupInfo()
const providers = status.providers.reduce((p, c) => {
if (c.terms) {
p[Amounts.parseOrThrow(c.terms.annualFee).currency] = c
}
return p
}, {} as ProvidersByCurrency)
//add all the known currency with no backup info const providers = status.providers.sort((a, b) => {
const list = await wxApi.listKnownCurrencies() if (a.paymentStatus.type === ProviderPaymentType.Paid && b.paymentStatus.type === ProviderPaymentType.Paid) {
const currencies = list.exchanges.map(e => e.name).concat(list.auditors.map(a => a.name)) return getStatusPaidOrder(a.paymentStatus, b.paymentStatus)
currencies.forEach(c => {
if (!providers[c]) {
providers[c] = undefined
} }
return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
}) })
setStatus({ deviceName: status.deviceId, providers }) setStatus({ deviceName: status.deviceId, providers })

View File

@ -40,46 +40,117 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
return r return r
} }
export const Example = createExample(TestedComponent, { export const LotOfProviders = createExample(TestedComponent, {
deviceName: "somedevicename", providers: [{
providers: { "active": true,
ARS: { "syncProviderBaseUrl": "http://sync.taler:9967/",
"active": true, "lastSuccessfulBackupTimestamp": {
"syncProviderBaseUrl": "http://sync.taler:9967/", "t_ms": 1625063925078
"lastSuccessfulBackupTimestamp": { },
"t_ms": 1625063925078 "paymentProposalIds": [
}, "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
"paymentProposalIds": [ ],
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" "paymentStatus": {
], "type": ProviderPaymentType.Paid,
"paymentStatus": { "paidUntil": {
"type": ProviderPaymentType.Paid, "t_ms": 1656599921000
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
} }
}, },
KUDOS: { "terms": {
"active": false, "annualFee": "ARS:1",
"syncProviderBaseUrl": "http://sync.demo.taler.net/", "storageLimitInMegabytes": 16,
"paymentProposalIds": [], "supportedProtocolVersion": "0.0"
"paymentStatus": { }
"type": ProviderPaymentType.Unpaid, }, {
}, "active": false,
"terms": { "syncProviderBaseUrl": "http://sync.demo.taler.net/",
"annualFee": "KUDOS:0.1", "paymentProposalIds": [],
"storageLimitInMegabytes": 16, "paymentStatus": {
"supportedProtocolVersion": "0.0" "type": ProviderPaymentType.Unpaid,
}
}, },
USD: undefined, "terms": {
EUR: undefined "annualFee": "KUDOS:0.1",
} "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},{
"active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},{
"active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},{
"active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.Unpaid,
},
"terms": {
"annualFee": "KUDOS:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},{
"active": false,
"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,
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": 1656599921000
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}]
});
export const Empty = createExample(TestedComponent, {
providers: []
});

View File

@ -15,53 +15,64 @@
*/ */
import { Timestamp } from "@gnu-taler/taler-util"; import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo } from "@gnu-taler/taler-wallet-core";
import { formatDuration, intervalToDuration } from "date-fns"; import { formatDuration, intervalToDuration } from "date-fns";
import { JSX, VNode } from "preact"; import { JSX, VNode } from "preact";
import { ProvidersByCurrency, useBackupStatus } from "../hooks/useProvidersByCurrency"; import { useBackupStatus } from "../hooks/useProvidersByCurrency";
import { Pages } from "./popup"; import { Pages } from "./popup";
export function BackupPage(): VNode { interface Props {
onAddProvider: () => void;
}
export function BackupPage({ onAddProvider }: Props): VNode {
const status = useBackupStatus() const status = useBackupStatus()
if (!status) { if (!status) {
return <div>Loading...</div> return <div>Loading...</div>
} }
return <BackupView deviceName={status.deviceName} providers={status.providers}/>; return <BackupView providers={status.providers} onAddProvider={onAddProvider} />;
} }
export interface ViewProps { export interface ViewProps {
deviceName: string; providers: ProviderInfo[],
providers: ProvidersByCurrency onAddProvider: () => void;
} }
export function BackupView({ deviceName, providers }: ViewProps): VNode { export function BackupView({ providers, onAddProvider }: ViewProps): VNode {
return ( return (
<div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}> <div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', justifyContent: 'space-between' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<h2 style={{ width: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 10, marginBottom:10 }}> <section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
{deviceName}
</h2> {!!providers.length && <div>
<div style={{ flexDirection: 'row', marginTop: 'auto', marginBottom: 'auto' }}> {providers.map((provider, idx) => {
<button class="pure-button button-secondary">rename</button> return <BackupLayout
</div> status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={idx}
active={provider.active}
subtitle={provider.syncProviderBaseUrl}
title={provider.syncProviderBaseUrl}
/>
})}
</div>}
{!providers.length && <div>
There is not backup providers configured, add one with the button below
</div>}
</section>
<footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
<button class="pure-button button-secondary" disabled={!providers.length} style={{ marginLeft: 5 }} onClick={onAddProvider}>{
providers.length > 1 ?
<i18n.Translate>sync all now</i18n.Translate>:
<i18n.Translate>sync now</i18n.Translate>
}</button>
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>add provider</i18n.Translate></button>
</div>
</footer>
</div> </div>
{Object.keys(providers).map((currency) => {
const provider = providers[currency]
if (!provider) {
return <BackupLayout
id={currency}
title={currency}
/>
}
return <BackupLayout
status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp}
id={currency}
active={provider.active}
subtitle={provider.syncProviderBaseUrl}
title={currency}
/>
})}
</div> </div>
) )
} }
@ -70,7 +81,7 @@ interface TransactionLayoutProps {
status?: any; status?: any;
timestamp?: Timestamp; timestamp?: Timestamp;
title: string; title: string;
id: string; id: number;
subtitle?: string; subtitle?: string;
active?: boolean; active?: boolean;
} }
@ -96,13 +107,13 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
<div <div
style={{ display: "flex", flexDirection: "column", color: !props.active ? "gray" : undefined }} style={{ display: "flex", flexDirection: "column", color: !props.active ? "gray" : undefined }}
> >
{dateStr && <div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>}
{!dateStr && <div style={{ fontSize: "small", color: "red" }}>never synced</div>}
<div style={{ fontVariant: "small-caps", fontSize: "x-large" }}> <div style={{ fontVariant: "small-caps", fontSize: "x-large" }}>
<a href={Pages.provider_detail.replace(':currency', props.id)}><span>{props.title}</span></a> <a href={Pages.provider_detail.replace(':pid', String(props.id))}><span>{props.title}</span></a>
</div> </div>
<div>{props.subtitle}</div> {dateStr && <div style={{ fontSize: "small" }}>Last time synced: {dateStr}</div>}
{!dateStr && <div style={{ fontSize: "small", color: "red" }}>never synced</div>}
</div> </div>
<div style={{ <div style={{
marginLeft: "auto", marginLeft: "auto",
@ -111,7 +122,7 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
alignItems: "center", alignItems: "center",
alignSelf: "center" alignSelf: "center"
}}> }}>
<div style={{}}> <div style={{ whiteSpace: 'nowrap' }}>
{!props.status ? "missing" : ( {!props.status ? "missing" : (
props.status?.type === 'paid' ? daysUntil(props.status.paidUntil) : 'unpaid' props.status?.type === 'paid' ? daysUntil(props.status.paidUntil) : 'unpaid'
)} )}

View File

@ -40,7 +40,6 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
} }
export const DemoService = createExample(TestedComponent, { export const DemoService = createExample(TestedComponent, {
currency: 'KUDOS',
url: 'https://sync.demo.taler.net/', url: 'https://sync.demo.taler.net/',
provider: { provider: {
annual_fee: 'KUDOS:0.1', annual_fee: 'KUDOS:0.1',
@ -50,7 +49,6 @@ export const DemoService = createExample(TestedComponent, {
}); });
export const FreeService = createExample(TestedComponent, { export const FreeService = createExample(TestedComponent, {
currency: 'ARS',
url: 'https://sync.taler:9667/', url: 'https://sync.taler:9667/',
provider: { provider: {
annual_fee: 'ARS:0', annual_fee: 'ARS:0',

View File

@ -1,37 +1,60 @@
import { Amounts, BackupBackupProviderTerms, i18n } from "@gnu-taler/taler-util"; import { Amounts, BackupBackupProviderTerms, i18n } from "@gnu-taler/taler-util";
import { privateDecrypt } from "crypto"; import { Fragment, VNode } from "preact";
import { add, addYears } from "date-fns";
import { VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
import ProviderAddConfirmProviderStories from "./ProviderAddConfirmProvider.stories";
interface Props { interface Props {
currency: string; currency: string;
} }
export function ProviderAddPage({ currency }: Props): VNode { 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({ }: Props): VNode {
const [verifying, setVerifying] = useState<{ url: string, provider: BackupBackupProviderTerms } | undefined>(undefined) const [verifying, setVerifying] = useState<{ url: string, provider: BackupBackupProviderTerms } | undefined>(undefined)
const [readingTerms, setReadingTerms] = useState<boolean | undefined>(undefined)
const alreadyCheckedTheTerms = readingTerms === false
if (!verifying) { if (!verifying) {
return <SetUrlView return <SetUrlView
currency={currency}
onCancel={() => { onCancel={() => {
setVerifying(undefined); setVerifying(undefined);
}} }}
onVerify={(url) => { onVerify={(url) => {
return fetch(url).then(r => r.json()) return fetch(`${url}/config`)
.then((provider) => setVerifying({ url, provider })) .catch(e => { throw new Error(`Network error`) })
.then(getJsonIfOk)
.then((provider) => { setVerifying({ url, provider }); return undefined })
.catch((e) => e.message) .catch((e) => e.message)
}} }}
/> />
} }
if (readingTerms) {
return <TermsOfService
onCancel={() => setReadingTerms(undefined)}
onAccept={() => setReadingTerms(false)}
/>
}
return <ConfirmProviderView return <ConfirmProviderView
provider={verifying.provider} provider={verifying.provider}
currency={currency} termsChecked={alreadyCheckedTheTerms}
url={verifying.url} url={verifying.url}
onCancel={() => { onCancel={() => {
setVerifying(undefined); setVerifying(undefined);
}} }}
onShowTerms={() => {
setReadingTerms(true)
}}
onConfirm={() => { onConfirm={() => {
wxApi.addBackupProvider(verifying.url).then(_ => history.go(-1)) wxApi.addBackupProvider(verifying.url).then(_ => history.go(-1))
}} }}
@ -39,33 +62,75 @@ export function ProviderAddPage({ currency }: Props): VNode {
/> />
} }
export interface SetUrlViewProps { interface TermsOfServiceProps {
currency: string,
onCancel: () => void; onCancel: () => void;
onVerify: (s: string) => Promise<string | undefined>; onAccept: () => void;
} }
export function SetUrlView({ currency, onCancel, onVerify }: SetUrlViewProps) { function TermsOfService({ onCancel, onAccept }: TermsOfServiceProps) {
const [value, setValue] = useState<string>("")
const [error, setError] = useState<string | undefined>(undefined)
return <div style={{ display: 'flex', flexDirection: 'column' }}> return <div style={{ display: 'flex', flexDirection: 'column' }}>
<section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
<div> <div>
Add backup provider for storing <b>{currency}</b> Here we will place the complete text of terms of service
</div> </div>
{error && <div class="errorbox" style={{ marginTop: 10 }} >
<p>{error}</p>
</div>}
<h3>Backup provider URL</h3>
<input style={{ width: 'calc(100% - 8px)' }} value={value} onChange={(e) => setValue(e.currentTarget.value)} />
<p>
Backup providers may charge for their service
</p>
</section> </section>
<footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
<button class="pure-button" onClick={onCancel}><i18n.Translate>cancel</i18n.Translate></button> <button class="pure-button" onClick={onCancel}><i18n.Translate>cancel</i18n.Translate></button>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
<button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={() => onVerify(value).then(r => r ? setError(r) : undefined)}><i18n.Translate>verify service terms</i18n.Translate></button> <button class="pure-button" onClick={onAccept}><i18n.Translate>accept</i18n.Translate></button>
</div>
</footer>
</div>
}
export interface SetUrlViewProps {
initialValue?: string;
onCancel: () => void;
onVerify: (s: string) => Promise<string | undefined>;
withError?: string;
}
import arrowDown from '../../static/img/chevron-down.svg';
export function SetUrlView({ initialValue, onCancel, onVerify, withError }: SetUrlViewProps) {
const [value, setValue] = useState<string>(initialValue || "")
const [error, setError] = useState<string | undefined>(withError)
const [showErrorDetail, setShowErrorDetail] = useState(false);
return <div style={{ display: 'flex', flexDirection: 'column' }}>
<section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
<div>
Add backup provider for saving coins
</div>
<h3>Backup provider URL</h3>
<div style={{ width: '3em', display: 'inline-block' }}>https://</div>
<input style={{ width: 'calc(100% - 8px - 4em)', marginLeft: 5 }} value={value} onChange={(e) => setValue(e.currentTarget.value)} />
<p>
Backup providers may charge for their service
</p>
{error && <Fragment>
<div class="errorbox" style={{ marginTop: 10 }} >
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'space-between', display: 'flex' }}>
<p style={{ alignSelf: 'center' }}>Could not get provider information</p>
<p>
<button style={{ fontSize: '100%', padding: 0, height: 28, width: 28 }} onClick={() => { setShowErrorDetail(v => !v) }} >
<img style={{ height: '1.5em' }} src={arrowDown} />
</button>
</p>
</div>
{showErrorDetail && <div>{error}</div>}
</div>
</Fragment>
}
</section>
<footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
<button class="pure-button" onClick={onCancel}><i18n.Translate>cancel</i18n.Translate></button>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
<button class="pure-button button-secondary" style={{ marginLeft: 5 }}
disabled={!value}
onClick={() => {
let url = value.startsWith('http://') || value.startsWith('https://') ? value : `https://${value}`
url = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
return onVerify(url).then(r => r ? setError(r) : undefined)
}}><i18n.Translate>next</i18n.Translate></button>
</div> </div>
</footer> </footer>
</div> </div>
@ -73,19 +138,16 @@ export function SetUrlView({ currency, onCancel, onVerify }: SetUrlViewProps) {
export interface ConfirmProviderViewProps { export interface ConfirmProviderViewProps {
provider: BackupBackupProviderTerms, provider: BackupBackupProviderTerms,
currency: string,
url: string, url: string,
onCancel: () => void; onCancel: () => void;
onConfirm: () => void onConfirm: () => void;
onShowTerms: () => void;
termsChecked: boolean;
} }
export function ConfirmProviderView({ url, provider, currency, onCancel, onConfirm }: ConfirmProviderViewProps) { export function ConfirmProviderView({ url, termsChecked, onShowTerms, provider, onCancel, onConfirm }: ConfirmProviderViewProps) {
return <div style={{ display: 'flex', flexDirection: 'column' }}> return <div style={{ display: 'flex', flexDirection: 'column' }}>
<section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
<div> <div>Verify provider service terms for <b>{url}</b> backup provider</div>
Verify provider service terms for storing <b>{currency}</b>
</div>
<h3>{url}</h3>
<p> <p>
{Amounts.isZero(provider.annual_fee) ? 'free of charge' : provider.annual_fee} for a year of backup service {Amounts.isZero(provider.annual_fee) ? 'free of charge' : provider.annual_fee} for a year of backup service
</p> </p>
@ -98,9 +160,14 @@ export function ConfirmProviderView({ url, provider, currency, onCancel, onConfi
<i18n.Translate>cancel</i18n.Translate> <i18n.Translate>cancel</i18n.Translate>
</button> </button>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onConfirm}> {termsChecked ?
<i18n.Translate>confirm</i18n.Translate> <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onConfirm}>
</button> <i18n.Translate>confirm</i18n.Translate>
</button> :
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onShowTerms}>
<i18n.Translate>review terms</i18n.Translate>
</button>
}
</div> </div>
</footer> </footer>
</div> </div>

View File

@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
import { SetUrlView as TestedComponent } from './ProviderAddPage'; import { SetUrlView as TestedComponent } from './ProviderAddPage';
@ -40,7 +39,21 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
return r return r
} }
export const SetUrl = createExample(TestedComponent, { export const Initial = createExample(TestedComponent, {
currency: 'ARS',
}); });
export const WithValue = createExample(TestedComponent, {
initialValue: 'sync.demo.taler.net'
});
export const WithConnectionError = createExample(TestedComponent, {
withError: 'Network error'
});
export const WithClientError = createExample(TestedComponent, {
withError: 'URL may not be right: (404) Not Found'
});
export const WithServerError = createExample(TestedComponent, {
withError: 'Try another server: (500) Internal Server Error'
});

View File

@ -40,12 +40,7 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
return r return r
} }
export const NotDefined = createExample(TestedComponent, {
currency: 'ARS',
});
export const Active = createExample(TestedComponent, { export const Active = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": true, "active": true,
"syncProviderBaseUrl": "http://sync.taler:9967/", "syncProviderBaseUrl": "http://sync.taler:9967/",
@ -62,7 +57,7 @@ export const Active = createExample(TestedComponent, {
} }
}, },
"terms": { "terms": {
"annualFee": "ARS:1", "annualFee": "EUR:1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -70,7 +65,6 @@ export const Active = createExample(TestedComponent, {
}); });
export const ActiveErrorSync = createExample(TestedComponent, { export const ActiveErrorSync = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": true, "active": true,
"syncProviderBaseUrl": "http://sync.taler:9967/", "syncProviderBaseUrl": "http://sync.taler:9967/",
@ -96,7 +90,7 @@ export const ActiveErrorSync = createExample(TestedComponent, {
message: 'message' message: 'message'
}, },
"terms": { "terms": {
"annualFee": "ARS:1", "annualFee": "EUR:1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -104,7 +98,6 @@ export const ActiveErrorSync = createExample(TestedComponent, {
}); });
export const ActiveBackupProblemUnreadable = createExample(TestedComponent, { export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": true, "active": true,
"syncProviderBaseUrl": "http://sync.taler:9967/", "syncProviderBaseUrl": "http://sync.taler:9967/",
@ -124,7 +117,7 @@ export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
type: 'backup-unreadable' type: 'backup-unreadable'
}, },
"terms": { "terms": {
"annualFee": "ARS:1", "annualFee": "EUR:1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -132,7 +125,6 @@ export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
}); });
export const ActiveBackupProblemDevice = createExample(TestedComponent, { export const ActiveBackupProblemDevice = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": true, "active": true,
"syncProviderBaseUrl": "http://sync.taler:9967/", "syncProviderBaseUrl": "http://sync.taler:9967/",
@ -157,7 +149,7 @@ export const ActiveBackupProblemDevice = createExample(TestedComponent, {
} }
}, },
"terms": { "terms": {
"annualFee": "ARS:1", "annualFee": "EUR:1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -165,7 +157,6 @@ export const ActiveBackupProblemDevice = createExample(TestedComponent, {
}); });
export const InactiveUnpaid = createExample(TestedComponent, { export const InactiveUnpaid = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": false, "active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/", "syncProviderBaseUrl": "http://sync.demo.taler.net/",
@ -174,7 +165,7 @@ export const InactiveUnpaid = createExample(TestedComponent, {
"type": ProviderPaymentType.Unpaid, "type": ProviderPaymentType.Unpaid,
}, },
"terms": { "terms": {
"annualFee": "ARS:0.1", "annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -182,7 +173,6 @@ export const InactiveUnpaid = createExample(TestedComponent, {
}); });
export const InactiveInsufficientBalance = createExample(TestedComponent, { export const InactiveInsufficientBalance = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": false, "active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/", "syncProviderBaseUrl": "http://sync.demo.taler.net/",
@ -191,7 +181,7 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
"type": ProviderPaymentType.InsufficientBalance, "type": ProviderPaymentType.InsufficientBalance,
}, },
"terms": { "terms": {
"annualFee": "ARS:0.1", "annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -199,7 +189,6 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
}); });
export const InactivePending = createExample(TestedComponent, { export const InactivePending = createExample(TestedComponent, {
currency: 'ARS',
info: { info: {
"active": false, "active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/", "syncProviderBaseUrl": "http://sync.demo.taler.net/",
@ -208,7 +197,7 @@ export const InactivePending = createExample(TestedComponent, {
"type": ProviderPaymentType.Pending, "type": ProviderPaymentType.Pending,
}, },
"terms": { "terms": {
"annualFee": "ARS:0.1", "annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
@ -216,3 +205,32 @@ export const InactivePending = createExample(TestedComponent, {
}); });
export const ActiveTermsChanged = createExample(TestedComponent, {
info: {
"active": true,
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [],
"paymentStatus": {
"type": ProviderPaymentType.TermsChanged,
paidUntil: {
t_ms: 1656599921000
},
newTerms: {
"annualFee": "EUR:10",
"storageLimitInMegabytes": 8,
"supportedProtocolVersion": "0.0"
},
oldTerms: {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},
"terms": {
"annualFee": "EUR:0.1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
}
});

View File

@ -16,7 +16,8 @@
import { BackupBackupProviderTerms, i18n, Timestamp } from "@gnu-taler/taler-util"; import { BackupBackupProviderTerms, i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; import { ProviderInfo, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import { ContractTermsUtil } from "@gnu-taler/taler-wallet-core/src/util/contractTerms";
import { formatDuration, intervalToDuration, format } from "date-fns"; import { formatDuration, intervalToDuration, format } from "date-fns";
import { Fragment, VNode } from "preact"; import { Fragment, VNode } from "preact";
import { useRef, useState } from "preact/hooks"; import { useRef, useState } from "preact/hooks";
@ -24,42 +25,45 @@ import { useBackupStatus } from "../hooks/useProvidersByCurrency";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
interface Props { interface Props {
currency: string; pid: string;
onAddProvider: (c: string) => void;
onBack: () => void; onBack: () => void;
} }
export function ProviderDetailPage({ currency, onAddProvider, onBack }: Props): VNode { export function ProviderDetailPage({ pid, onBack }: Props): VNode {
const status = useBackupStatus() const status = useBackupStatus()
if (!status) { if (!status) {
return <div>Loading...</div> return <div>Loading...</div>
} }
const info = status.providers[currency]; const idx = parseInt(pid, 10)
return <ProviderView currency={currency} info={info} if (Number.isNaN(idx) || !(status.providers[idx])) {
onBack()
return <div />
}
const info = status.providers[idx];
return <ProviderView info={info}
onSync={() => { null }} onSync={() => { null }}
onDelete={() => { null }} onDelete={() => { null }}
onBack={onBack} onBack={onBack}
onAddProvider={() => onAddProvider(currency)} onExtend={() => { null }}
/>; />;
} }
export interface ViewProps { export interface ViewProps {
currency: string; info: ProviderInfo;
info?: ProviderInfo;
onDelete: () => void; onDelete: () => void;
onSync: () => void; onSync: () => void;
onBack: () => void; onBack: () => void;
onAddProvider: () => void; onExtend: () => void;
} }
export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddProvider }: ViewProps): VNode { export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode {
function Footer() { function Footer() {
return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
<button class="pure-button" onClick={onBack}><i18n.Translate>back</i18n.Translate></button> <button class="pure-button" onClick={onBack}><i18n.Translate>back</i18n.Translate></button>
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
{info && <button class="pure-button button-destructive" onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>} {info && <button class="pure-button button-destructive" disabled onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>}
{info && <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>} {info && <button class="pure-button button-secondary" disabled style={{ marginLeft: 5 }} onClick={onExtend}><i18n.Translate>extend</i18n.Translate></button>}
{!info && <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>add provider</i18n.Translate></button>} {info && <button class="pure-button button-secondary" disabled style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>}
</div> </div>
</footer> </footer>
} }
@ -67,7 +71,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
if (info?.lastError) { if (info?.lastError) {
return <Fragment> return <Fragment>
<div class="errorbox" style={{ marginTop: 10 }} > <div class="errorbox" style={{ marginTop: 10 }} >
<div style={{ height: 0, textAlign: 'right', color: 'gray', fontSize: 'small' }}>{!info.lastAttemptedBackupTimestamp || info.lastAttemptedBackupTimestamp.t_ms === 'never' ? 'never' : format(new Date(info.lastAttemptedBackupTimestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</div> <div style={{ height: 0, textAlign: 'right', color: 'gray', fontSize: 'small' }}>last time tried {!info.lastAttemptedBackupTimestamp || info.lastAttemptedBackupTimestamp.t_ms === 'never' ? 'never' : format(new Date(info.lastAttemptedBackupTimestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</div>
<p>{info.lastError.hint}</p> <p>{info.lastError.hint}</p>
</div> </div>
</Fragment> </Fragment>
@ -76,7 +80,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
switch (info.backupProblem.type) { switch (info.backupProblem.type) {
case "backup-conflicting-device": case "backup-conflicting-device":
return <div class="errorbox" style={{ marginTop: 10 }}> return <div class="errorbox" style={{ marginTop: 10 }}>
<p>There is another backup from <b>{info.backupProblem.otherDeviceId}</b></p> <p>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></p>
</div> </div>
case "backup-unreadable": case "backup-unreadable":
return <div class="errorbox" style={{ marginTop: 10 }}> return <div class="errorbox" style={{ marginTop: 10 }}>
@ -84,7 +88,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
</div> </div>
default: default:
return <div class="errorbox" style={{ marginTop: 10 }}> return <div class="errorbox" style={{ marginTop: 10 }}>
<p>Unkown backup problem: {JSON.stringify(info.backupProblem)}</p> <p>Unknown backup problem: {JSON.stringify(info.backupProblem)}</p>
</div> </div>
} }
} }
@ -110,6 +114,28 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
return undefined return undefined
} }
function descriptionByStatus(status: ProviderPaymentStatus | undefined) {
if (!status) return ''
switch (status.type) {
case ProviderPaymentType.InsufficientBalance:
return 'no enough balance to make the payment'
case ProviderPaymentType.Unpaid:
return 'not pay yet'
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
if (status.paidUntil.t_ms === 'never') {
return 'service paid.'
} else {
return `service paid until ${format(status.paidUntil.t_ms, 'yyyy/MM/dd HH:mm:ss')}`
}
case ProviderPaymentType.Pending:
return ''
default:
break;
}
return undefined
}
return ( return (
<div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}> <div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
<style>{` <style>{`
@ -120,18 +146,49 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
<span style={{ padding: 5, display: 'inline-block', backgroundColor: colorByStatus(info?.paymentStatus.type), borderRadius: 5, color: 'white' }}>{info?.paymentStatus.type}</span> <span style={{ padding: 5, display: 'inline-block', backgroundColor: colorByStatus(info?.paymentStatus.type), borderRadius: 5, color: 'white' }}>{info?.paymentStatus.type}</span>
{info && <span style={{ float: "right", fontSize: "small", color: "gray", padding: 5 }}> {/* {info && <span style={{ float: "right", fontSize: "small", color: "gray", padding: 5 }}>
From <b>{info.syncProviderBaseUrl}</b> From <b>{info.syncProviderBaseUrl}</b>
</span>} </span>} */}
{info && <div style={{ float: 'right', fontSize: "large", padding: 5 }}>{info.terms?.annualFee} / year</div>}
<Error /> <Error />
<h3>{info?.syncProviderBaseUrl}</h3>
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", }}> <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", }}>
<h1>{currency}</h1> <div>{daysSince(info?.lastSuccessfulBackupTimestamp)} </div>
{info && <div style={{ marginTop: 'auto', marginBottom: 'auto' }}>{info.terms?.annualFee} / year</div>}
</div> </div>
<div>{daysSince(info?.lastSuccessfulBackupTimestamp)} </div> <p>{descriptionByStatus(info?.paymentStatus)}</p>
{info?.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
<p>terms has changed, extending the service will imply accepting the new terms of service</p>
<table>
<thead>
<tr>
<td></td>
<td>old</td>
<td> -&gt;</td>
<td>new</td>
</tr>
</thead>
<tbody>
<tr>
<td>fee</td>
<td>{info.paymentStatus.oldTerms.annualFee}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.annualFee}</td>
</tr>
<tr>
<td>storage</td>
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
<td>-&gt;</td>
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
</tr>
</tbody>
</table>
</div>}
</section> </section>
<Footer /> <Footer />
</div> </div>

View File

@ -19,13 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import {
PaymentStatus,
TransactionCommon, TransactionDeposit, TransactionPayment,
TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
TransactionWithdrawal,
WithdrawalType
} from '@gnu-taler/taler-util';
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
import { SettingsView as TestedComponent } from './Settings'; import { SettingsView as TestedComponent } from './Settings';
@ -33,9 +26,7 @@ export default {
title: 'popup/settings', title: 'popup/settings',
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: 'onRetry' }, setDeviceName: () => Promise.resolve(),
onDelete: { action: 'onDelete' },
onBack: { action: 'onBack' },
} }
}; };
@ -46,9 +37,14 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
return r return r
} }
export const AllOff = createExample(TestedComponent, {}); export const AllOff = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
export const OneChecked = createExample(TestedComponent, { setDeviceName: () => Promise.resolve(),
permissionsEnabled: true, });
export const OneChecked = createExample(TestedComponent, {
deviceName: 'this-is-the-device-name',
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
}); });

View File

@ -17,40 +17,57 @@
import { VNode } from "preact"; import { VNode } from "preact";
import { Checkbox } from "../components/Checkbox"; import { Checkbox } from "../components/Checkbox";
import { EditableText } from "../components/EditableText";
import { useDevContext } from "../context/useDevContext"; import { useDevContext } from "../context/useDevContext";
import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
export function SettingsPage(): VNode { export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
const { devMode, toggleDevMode } = useDevContext() const { devMode, toggleDevMode } = useDevContext()
const { name, update } = useBackupDeviceName()
return <SettingsView return <SettingsView
deviceName={name} setDeviceName={update}
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions} permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
developerMode={devMode} toggleDeveloperMode={toggleDevMode} developerMode={devMode} toggleDeveloperMode={toggleDevMode}
/>; />;
} }
export interface ViewProps { export interface ViewProps {
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
permissionsEnabled: boolean; permissionsEnabled: boolean;
togglePermissions: () => void; togglePermissions: () => void;
developerMode: boolean; developerMode: boolean;
toggleDeveloperMode: () => void; toggleDeveloperMode: () => void;
} }
export function SettingsView({permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode}: ViewProps): VNode { export function SettingsView({ deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
return ( return (
<div> <div>
<h2>Permissions</h2> <section style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
<Checkbox label="Automatically open wallet based on page content"
name="perm" <h2>Wallet</h2>
description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)" <EditableText
enabled={permissionsEnabled} onToggle={togglePermissions} value={deviceName}
/> onChange={setDeviceName}
<h2>Config</h2> name="device-id"
<Checkbox label="Developer mode" label="Device name"
name="devMode" description="(This is how you will recognize the wallet in the backup provider)"
description="(More options and information useful for debugging)" />
enabled={developerMode} onToggle={toggleDeveloperMode} <h2>Permissions</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> </div>
) )
} }

View File

@ -36,8 +36,8 @@ export enum Pages {
backup = '/backup', backup = '/backup',
history = '/history', history = '/history',
transaction = '/transaction/:tid', transaction = '/transaction/:tid',
provider_detail = '/provider/:currency', provider_detail = '/provider/:pid',
provider_add = '/provider/:currency/add', provider_add = '/provider/add',
} }
interface TabProps { interface TabProps {
@ -61,7 +61,6 @@ function Tab(props: TabProps): JSX.Element {
export function WalletNavBar() { export function WalletNavBar() {
const { devMode } = useDevContext() const { devMode } = useDevContext()
return <Match>{({ path }: any) => { return <Match>{({ path }: any) => {
console.log("current", path)
return ( return (
<div class="nav" id="header"> <div class="nav" id="header">
<Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab> <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>

View File

@ -99,8 +99,16 @@ function Application() {
<Route path={Pages.settings} component={SettingsPage} /> <Route path={Pages.settings} component={SettingsPage} />
<Route path={Pages.dev} component={DeveloperPage} /> <Route path={Pages.dev} component={DeveloperPage} />
<Route path={Pages.history} component={HistoryPage} /> <Route path={Pages.history} component={HistoryPage} />
<Route path={Pages.backup} component={BackupPage} /> <Route path={Pages.backup} component={BackupPage}
<Route path={Pages.provider_detail} component={ProviderDetailPage} /> onAddProvider={() => {
route(Pages.provider_add)
}}
/>
<Route path={Pages.provider_detail} component={ProviderDetailPage}
onBack={() => {
route(Pages.backup)
}}
/>
<Route path={Pages.provider_add} component={ProviderAddPage} /> <Route path={Pages.provider_add} component={ProviderAddPage} />
<Route path={Pages.transaction} component={TransactionPage} /> <Route path={Pages.transaction} component={TransactionPage} />
<Route default component={Redirect} to={Pages.balance} /> <Route default component={Redirect} to={Pages.balance} />

View File

@ -37,6 +37,7 @@ import {
AcceptTipRequest, AcceptTipRequest,
DeleteTransactionRequest, DeleteTransactionRequest,
RetryTransactionRequest, RetryTransactionRequest,
SetWalletDeviceIdRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError } from "@gnu-taler/taler-wallet-core"; import { AddBackupProviderRequest, BackupProviderState, OperationFailedError } from "@gnu-taler/taler-wallet-core";
import { BackupInfo } from "@gnu-taler/taler-wallet-core"; import { BackupInfo } from "@gnu-taler/taler-wallet-core";
@ -179,13 +180,17 @@ export function addBackupProvider(backupProviderBaseUrl: string): Promise<void>
} as AddBackupProviderRequest) } as AddBackupProviderRequest)
} }
export function setWalletDeviceId(walletDeviceId: string): Promise<void> {
return callBackend("setWalletDeviceId", {
walletDeviceId
} as SetWalletDeviceIdRequest)
}
export function syncAllProviders(): Promise<void> { export function syncAllProviders(): Promise<void> {
return callBackend("runBackupCycle", {}) return callBackend("runBackupCycle", {})
} }
/** /**
* Retry a transaction * Retry a transaction
* @param transactionId * @param transactionId

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="92px" height="92px" viewBox="0 0 92 92" enable-background="new 0 0 92 92" xml:space="preserve">
<path id="XMLID_467_" d="M46,63c-1.1,0-2.1-0.4-2.9-1.2l-25-26c-1.5-1.6-1.5-4.1,0.1-5.7c1.6-1.5,4.1-1.5,5.7,0.1l22.1,23l22.1-23
c1.5-1.6,4.1-1.6,5.7-0.1c1.6,1.5,1.6,4.1,0.1,5.7l-25,26C48.1,62.6,47.1,63,46,63z"/>
</svg>

After

Width:  |  Height:  |  Size: 584 B