refactored backup sync UI
This commit is contained in:
parent
550905f0e7
commit
678a90934c
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -1,37 +1,43 @@
|
||||
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 * as wxApi from "../wxApi";
|
||||
|
||||
export interface ProvidersByCurrency {
|
||||
[s: string]: ProviderInfo | undefined
|
||||
}
|
||||
export interface BackupStatus {
|
||||
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 {
|
||||
const [status, setStatus] = useState<BackupStatus | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
//create a first list of backup info by currency
|
||||
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 list = await wxApi.listKnownCurrencies()
|
||||
const currencies = list.exchanges.map(e => e.name).concat(list.auditors.map(a => a.name))
|
||||
currencies.forEach(c => {
|
||||
if (!providers[c]) {
|
||||
providers[c] = undefined
|
||||
const providers = status.providers.sort((a, b) => {
|
||||
if (a.paymentStatus.type === ProviderPaymentType.Paid && b.paymentStatus.type === ProviderPaymentType.Paid) {
|
||||
return getStatusPaidOrder(a.paymentStatus, b.paymentStatus)
|
||||
}
|
||||
return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
|
||||
})
|
||||
|
||||
setStatus({ deviceName: status.deviceId, providers })
|
||||
|
@ -40,46 +40,117 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
|
||||
return r
|
||||
}
|
||||
|
||||
export const Example = createExample(TestedComponent, {
|
||||
deviceName: "somedevicename",
|
||||
providers: {
|
||||
ARS: {
|
||||
"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 LotOfProviders = createExample(TestedComponent, {
|
||||
providers: [{
|
||||
"active": true,
|
||||
"syncProviderBaseUrl": "http://sync.taler:9967/",
|
||||
"lastSuccessfulBackupTimestamp": {
|
||||
"t_ms": 1625063925078
|
||||
},
|
||||
"paymentProposalIds": [
|
||||
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
|
||||
],
|
||||
"paymentStatus": {
|
||||
"type": ProviderPaymentType.Paid,
|
||||
"paidUntil": {
|
||||
"t_ms": 1656599921000
|
||||
}
|
||||
},
|
||||
KUDOS: {
|
||||
"active": false,
|
||||
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
|
||||
"paymentProposalIds": [],
|
||||
"paymentStatus": {
|
||||
"type": ProviderPaymentType.Unpaid,
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "KUDOS:0.1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
"terms": {
|
||||
"annualFee": "ARS:1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
}, {
|
||||
"active": false,
|
||||
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
|
||||
"paymentProposalIds": [],
|
||||
"paymentStatus": {
|
||||
"type": ProviderPaymentType.Unpaid,
|
||||
},
|
||||
USD: undefined,
|
||||
EUR: undefined
|
||||
}
|
||||
"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"
|
||||
}
|
||||
},{
|
||||
"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: []
|
||||
});
|
||||
|
||||
|
@ -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 { JSX, VNode } from "preact";
|
||||
import { ProvidersByCurrency, useBackupStatus } from "../hooks/useProvidersByCurrency";
|
||||
import { useBackupStatus } from "../hooks/useProvidersByCurrency";
|
||||
import { Pages } from "./popup";
|
||||
|
||||
export function BackupPage(): VNode {
|
||||
interface Props {
|
||||
onAddProvider: () => void;
|
||||
}
|
||||
|
||||
export function BackupPage({ onAddProvider }: Props): VNode {
|
||||
const status = useBackupStatus()
|
||||
if (!status) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return <BackupView deviceName={status.deviceName} providers={status.providers}/>;
|
||||
return <BackupView providers={status.providers} onAddProvider={onAddProvider} />;
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
deviceName: string;
|
||||
providers: ProvidersByCurrency
|
||||
providers: ProviderInfo[],
|
||||
onAddProvider: () => void;
|
||||
}
|
||||
|
||||
export function BackupView({ deviceName, providers }: ViewProps): VNode {
|
||||
export function BackupView({ providers, onAddProvider }: ViewProps): VNode {
|
||||
return (
|
||||
<div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', justifyContent: 'space-between' }}>
|
||||
<h2 style={{ width: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 10, marginBottom:10 }}>
|
||||
{deviceName}
|
||||
</h2>
|
||||
<div style={{ flexDirection: 'row', marginTop: 'auto', marginBottom: 'auto' }}>
|
||||
<button class="pure-button button-secondary">rename</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
|
||||
|
||||
{!!providers.length && <div>
|
||||
{providers.map((provider, idx) => {
|
||||
return <BackupLayout
|
||||
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>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
@ -70,7 +81,7 @@ interface TransactionLayoutProps {
|
||||
status?: any;
|
||||
timestamp?: Timestamp;
|
||||
title: string;
|
||||
id: string;
|
||||
id: number;
|
||||
subtitle?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
@ -96,13 +107,13 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
|
||||
<div
|
||||
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" }}>
|
||||
<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>{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 style={{
|
||||
marginLeft: "auto",
|
||||
@ -111,7 +122,7 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
|
||||
alignItems: "center",
|
||||
alignSelf: "center"
|
||||
}}>
|
||||
<div style={{}}>
|
||||
<div style={{ whiteSpace: 'nowrap' }}>
|
||||
{!props.status ? "missing" : (
|
||||
props.status?.type === 'paid' ? daysUntil(props.status.paidUntil) : 'unpaid'
|
||||
)}
|
||||
|
@ -40,7 +40,6 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
|
||||
}
|
||||
|
||||
export const DemoService = createExample(TestedComponent, {
|
||||
currency: 'KUDOS',
|
||||
url: 'https://sync.demo.taler.net/',
|
||||
provider: {
|
||||
annual_fee: 'KUDOS:0.1',
|
||||
@ -50,7 +49,6 @@ export const DemoService = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const FreeService = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
url: 'https://sync.taler:9667/',
|
||||
provider: {
|
||||
annual_fee: 'ARS:0',
|
||||
|
@ -1,37 +1,60 @@
|
||||
import { Amounts, BackupBackupProviderTerms, i18n } from "@gnu-taler/taler-util";
|
||||
import { privateDecrypt } from "crypto";
|
||||
import { add, addYears } from "date-fns";
|
||||
import { VNode } from "preact";
|
||||
import { Fragment, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import * as wxApi from "../wxApi";
|
||||
import ProviderAddConfirmProviderStories from "./ProviderAddConfirmProvider.stories";
|
||||
|
||||
interface Props {
|
||||
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 [readingTerms, setReadingTerms] = useState<boolean | undefined>(undefined)
|
||||
const alreadyCheckedTheTerms = readingTerms === false
|
||||
|
||||
if (!verifying) {
|
||||
return <SetUrlView
|
||||
currency={currency}
|
||||
onCancel={() => {
|
||||
setVerifying(undefined);
|
||||
}}
|
||||
onVerify={(url) => {
|
||||
return fetch(url).then(r => r.json())
|
||||
.then((provider) => setVerifying({ url, provider }))
|
||||
return fetch(`${url}/config`)
|
||||
.catch(e => { throw new Error(`Network error`) })
|
||||
.then(getJsonIfOk)
|
||||
.then((provider) => { setVerifying({ url, provider }); return undefined })
|
||||
.catch((e) => e.message)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
if (readingTerms) {
|
||||
return <TermsOfService
|
||||
onCancel={() => setReadingTerms(undefined)}
|
||||
onAccept={() => setReadingTerms(false)}
|
||||
/>
|
||||
}
|
||||
return <ConfirmProviderView
|
||||
provider={verifying.provider}
|
||||
currency={currency}
|
||||
termsChecked={alreadyCheckedTheTerms}
|
||||
url={verifying.url}
|
||||
onCancel={() => {
|
||||
setVerifying(undefined);
|
||||
}}
|
||||
onShowTerms={() => {
|
||||
setReadingTerms(true)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
wxApi.addBackupProvider(verifying.url).then(_ => history.go(-1))
|
||||
}}
|
||||
@ -39,33 +62,75 @@ export function ProviderAddPage({ currency }: Props): VNode {
|
||||
/>
|
||||
}
|
||||
|
||||
export interface SetUrlViewProps {
|
||||
currency: string,
|
||||
interface TermsOfServiceProps {
|
||||
onCancel: () => void;
|
||||
onVerify: (s: string) => Promise<string | undefined>;
|
||||
onAccept: () => void;
|
||||
}
|
||||
|
||||
export function SetUrlView({ currency, onCancel, onVerify }: SetUrlViewProps) {
|
||||
const [value, setValue] = useState<string>("")
|
||||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
function TermsOfService({ onCancel, onAccept }: TermsOfServiceProps) {
|
||||
return <div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
|
||||
<div>
|
||||
Add backup provider for storing <b>{currency}</b>
|
||||
Here we will place the complete text of terms of service
|
||||
</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>
|
||||
<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 }} 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>
|
||||
</footer>
|
||||
</div>
|
||||
@ -73,19 +138,16 @@ export function SetUrlView({ currency, onCancel, onVerify }: SetUrlViewProps) {
|
||||
|
||||
export interface ConfirmProviderViewProps {
|
||||
provider: BackupBackupProviderTerms,
|
||||
currency: string,
|
||||
url: string,
|
||||
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' }}>
|
||||
|
||||
<section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}>
|
||||
<div>
|
||||
Verify provider service terms for storing <b>{currency}</b>
|
||||
</div>
|
||||
<h3>{url}</h3>
|
||||
<div>Verify provider service terms for <b>{url}</b> backup provider</div>
|
||||
<p>
|
||||
{Amounts.isZero(provider.annual_fee) ? 'free of charge' : provider.annual_fee} for a year of backup service
|
||||
</p>
|
||||
@ -98,9 +160,14 @@ export function ConfirmProviderView({ url, provider, currency, onCancel, onConfi
|
||||
<i18n.Translate>cancel</i18n.Translate>
|
||||
</button>
|
||||
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
|
||||
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onConfirm}>
|
||||
<i18n.Translate>confirm</i18n.Translate>
|
||||
</button>
|
||||
{termsChecked ?
|
||||
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onConfirm}>
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -19,7 +19,6 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { SetUrlView as TestedComponent } from './ProviderAddPage';
|
||||
|
||||
@ -40,7 +39,21 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
|
||||
return r
|
||||
}
|
||||
|
||||
export const SetUrl = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
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'
|
||||
});
|
||||
|
@ -40,12 +40,7 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
|
||||
return r
|
||||
}
|
||||
|
||||
export const NotDefined = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
});
|
||||
|
||||
export const Active = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": true,
|
||||
"syncProviderBaseUrl": "http://sync.taler:9967/",
|
||||
@ -62,7 +57,7 @@ export const Active = createExample(TestedComponent, {
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:1",
|
||||
"annualFee": "EUR:1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -70,7 +65,6 @@ export const Active = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const ActiveErrorSync = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": true,
|
||||
"syncProviderBaseUrl": "http://sync.taler:9967/",
|
||||
@ -96,7 +90,7 @@ export const ActiveErrorSync = createExample(TestedComponent, {
|
||||
message: 'message'
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:1",
|
||||
"annualFee": "EUR:1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -104,7 +98,6 @@ export const ActiveErrorSync = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": true,
|
||||
"syncProviderBaseUrl": "http://sync.taler:9967/",
|
||||
@ -124,7 +117,7 @@ export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
|
||||
type: 'backup-unreadable'
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:1",
|
||||
"annualFee": "EUR:1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -132,7 +125,6 @@ export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const ActiveBackupProblemDevice = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": true,
|
||||
"syncProviderBaseUrl": "http://sync.taler:9967/",
|
||||
@ -157,7 +149,7 @@ export const ActiveBackupProblemDevice = createExample(TestedComponent, {
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:1",
|
||||
"annualFee": "EUR:1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -165,7 +157,6 @@ export const ActiveBackupProblemDevice = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const InactiveUnpaid = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": false,
|
||||
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
|
||||
@ -174,7 +165,7 @@ export const InactiveUnpaid = createExample(TestedComponent, {
|
||||
"type": ProviderPaymentType.Unpaid,
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:0.1",
|
||||
"annualFee": "EUR:0.1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -182,7 +173,6 @@ export const InactiveUnpaid = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const InactiveInsufficientBalance = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": false,
|
||||
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
|
||||
@ -191,7 +181,7 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
|
||||
"type": ProviderPaymentType.InsufficientBalance,
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:0.1",
|
||||
"annualFee": "EUR:0.1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"supportedProtocolVersion": "0.0"
|
||||
}
|
||||
@ -199,7 +189,6 @@ export const InactiveInsufficientBalance = createExample(TestedComponent, {
|
||||
});
|
||||
|
||||
export const InactivePending = createExample(TestedComponent, {
|
||||
currency: 'ARS',
|
||||
info: {
|
||||
"active": false,
|
||||
"syncProviderBaseUrl": "http://sync.demo.taler.net/",
|
||||
@ -208,7 +197,7 @@ export const InactivePending = createExample(TestedComponent, {
|
||||
"type": ProviderPaymentType.Pending,
|
||||
},
|
||||
"terms": {
|
||||
"annualFee": "ARS:0.1",
|
||||
"annualFee": "EUR:0.1",
|
||||
"storageLimitInMegabytes": 16,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,8 @@
|
||||
|
||||
|
||||
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 { Fragment, VNode } from "preact";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
@ -24,42 +25,45 @@ import { useBackupStatus } from "../hooks/useProvidersByCurrency";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface Props {
|
||||
currency: string;
|
||||
onAddProvider: (c: string) => void;
|
||||
pid: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ProviderDetailPage({ currency, onAddProvider, onBack }: Props): VNode {
|
||||
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
|
||||
const status = useBackupStatus()
|
||||
if (!status) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
const info = status.providers[currency];
|
||||
return <ProviderView currency={currency} info={info}
|
||||
const idx = parseInt(pid, 10)
|
||||
if (Number.isNaN(idx) || !(status.providers[idx])) {
|
||||
onBack()
|
||||
return <div />
|
||||
}
|
||||
const info = status.providers[idx];
|
||||
return <ProviderView info={info}
|
||||
onSync={() => { null }}
|
||||
onDelete={() => { null }}
|
||||
onBack={onBack}
|
||||
onAddProvider={() => onAddProvider(currency)}
|
||||
onExtend={() => { null }}
|
||||
/>;
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
currency: string;
|
||||
info?: ProviderInfo;
|
||||
info: ProviderInfo;
|
||||
onDelete: () => void;
|
||||
onSync: () => 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() {
|
||||
return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
|
||||
<button class="pure-button" onClick={onBack}><i18n.Translate>back</i18n.Translate></button>
|
||||
<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-secondary" style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</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-destructive" disabled onClick={onDelete}><i18n.Translate>remove</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-secondary" disabled style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>}
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
@ -67,7 +71,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
|
||||
if (info?.lastError) {
|
||||
return <Fragment>
|
||||
<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>
|
||||
</div>
|
||||
</Fragment>
|
||||
@ -76,7 +80,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
|
||||
switch (info.backupProblem.type) {
|
||||
case "backup-conflicting-device":
|
||||
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>
|
||||
case "backup-unreadable":
|
||||
return <div class="errorbox" style={{ marginTop: 10 }}>
|
||||
@ -84,7 +88,7 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
|
||||
</div>
|
||||
default:
|
||||
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>
|
||||
}
|
||||
}
|
||||
@ -110,6 +114,28 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
|
||||
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 (
|
||||
<div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
|
||||
<style>{`
|
||||
@ -120,18 +146,49 @@ export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddPr
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<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>
|
||||
{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>
|
||||
</span>}
|
||||
</span>} */}
|
||||
{info && <div style={{ float: 'right', fontSize: "large", padding: 5 }}>{info.terms?.annualFee} / year</div>}
|
||||
|
||||
<Error />
|
||||
|
||||
<h3>{info?.syncProviderBaseUrl}</h3>
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", }}>
|
||||
<h1>{currency}</h1>
|
||||
{info && <div style={{ marginTop: 'auto', marginBottom: 'auto' }}>{info.terms?.annualFee} / year</div>}
|
||||
<div>{daysSince(info?.lastSuccessfulBackupTimestamp)} </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> -></td>
|
||||
<td>new</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>fee</td>
|
||||
<td>{info.paymentStatus.oldTerms.annualFee}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.annualFee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>storage</td>
|
||||
<td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
|
||||
<td>-></td>
|
||||
<td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>}
|
||||
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
|
@ -19,13 +19,6 @@
|
||||
* @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 { SettingsView as TestedComponent } from './Settings';
|
||||
|
||||
@ -33,9 +26,7 @@ export default {
|
||||
title: 'popup/settings',
|
||||
component: TestedComponent,
|
||||
argTypes: {
|
||||
onRetry: { action: 'onRetry' },
|
||||
onDelete: { action: 'onDelete' },
|
||||
onBack: { action: 'onBack' },
|
||||
setDeviceName: () => Promise.resolve(),
|
||||
}
|
||||
};
|
||||
|
||||
@ -46,9 +37,14 @@ function createExample<Props>(Component: FunctionalComponent<Props>, props: Part
|
||||
return r
|
||||
}
|
||||
|
||||
export const AllOff = createExample(TestedComponent, {});
|
||||
|
||||
export const OneChecked = createExample(TestedComponent, {
|
||||
permissionsEnabled: true,
|
||||
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(),
|
||||
});
|
||||
|
||||
|
@ -17,40 +17,57 @@
|
||||
|
||||
import { VNode } from "preact";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { EditableText } from "../components/EditableText";
|
||||
import { useDevContext } from "../context/useDevContext";
|
||||
import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
|
||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||
|
||||
export function SettingsPage(): VNode {
|
||||
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
|
||||
const { devMode, toggleDevMode } = useDevContext()
|
||||
return <SettingsView
|
||||
const { name, update } = useBackupDeviceName()
|
||||
return <SettingsView
|
||||
deviceName={name} setDeviceName={update}
|
||||
permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
|
||||
developerMode={devMode} toggleDeveloperMode={toggleDevMode}
|
||||
/>;
|
||||
}
|
||||
|
||||
export interface ViewProps {
|
||||
deviceName: string;
|
||||
setDeviceName: (s: string) => Promise<void>;
|
||||
permissionsEnabled: boolean;
|
||||
togglePermissions: () => void;
|
||||
developerMode: boolean;
|
||||
toggleDeveloperMode: () => void;
|
||||
}
|
||||
|
||||
export function SettingsView({permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode}: ViewProps): VNode {
|
||||
export function SettingsView({ deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
|
||||
return (
|
||||
<div>
|
||||
<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 style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}>
|
||||
|
||||
<h2>Wallet</h2>
|
||||
<EditableText
|
||||
value={deviceName}
|
||||
onChange={setDeviceName}
|
||||
name="device-id"
|
||||
label="Device name"
|
||||
description="(This is how you will recognize the wallet in the backup provider)"
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
@ -36,8 +36,8 @@ export enum Pages {
|
||||
backup = '/backup',
|
||||
history = '/history',
|
||||
transaction = '/transaction/:tid',
|
||||
provider_detail = '/provider/:currency',
|
||||
provider_add = '/provider/:currency/add',
|
||||
provider_detail = '/provider/:pid',
|
||||
provider_add = '/provider/add',
|
||||
}
|
||||
|
||||
interface TabProps {
|
||||
@ -61,7 +61,6 @@ function Tab(props: TabProps): JSX.Element {
|
||||
export function WalletNavBar() {
|
||||
const { devMode } = useDevContext()
|
||||
return <Match>{({ path }: any) => {
|
||||
console.log("current", path)
|
||||
return (
|
||||
<div class="nav" id="header">
|
||||
<Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
|
||||
|
@ -99,8 +99,16 @@ function Application() {
|
||||
<Route path={Pages.settings} component={SettingsPage} />
|
||||
<Route path={Pages.dev} component={DeveloperPage} />
|
||||
<Route path={Pages.history} component={HistoryPage} />
|
||||
<Route path={Pages.backup} component={BackupPage} />
|
||||
<Route path={Pages.provider_detail} component={ProviderDetailPage} />
|
||||
<Route path={Pages.backup} component={BackupPage}
|
||||
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.transaction} component={TransactionPage} />
|
||||
<Route default component={Redirect} to={Pages.balance} />
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
AcceptTipRequest,
|
||||
DeleteTransactionRequest,
|
||||
RetryTransactionRequest,
|
||||
SetWalletDeviceIdRequest,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError } 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)
|
||||
}
|
||||
|
||||
export function setWalletDeviceId(walletDeviceId: string): Promise<void> {
|
||||
return callBackend("setWalletDeviceId", {
|
||||
walletDeviceId
|
||||
} as SetWalletDeviceIdRequest)
|
||||
}
|
||||
|
||||
export function syncAllProviders(): Promise<void> {
|
||||
return callBackend("runBackupCycle", {})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retry a transaction
|
||||
* @param transactionId
|
||||
|
@ -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 |
Loading…
Reference in New Issue
Block a user