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 { 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 })
|
||||||
|
@ -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: []
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -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'
|
||||||
)}
|
)}
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
});
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -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> -></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>
|
</section>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
|
@ -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
|
||||||
|
@ -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