add more backup stories, sync by one provider

This commit is contained in:
Sebastian 2021-07-08 15:23:53 -03:00
parent ba995882ba
commit 655c5fc18a
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
9 changed files with 143 additions and 56 deletions

View File

@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This file is in the public domain. # This file is in the public domain.
[ "also-wallet" == "$1" ] && { pnpm -C ../taler-wallet-core/ compile || exit 1; }
pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh && (cd extension/ && unzip taler*.zip) pnpm clean && pnpm compile && rm -rf extension/ && ./pack.sh && (cd extension/ && unzip taler*.zip)

View File

@ -1,12 +1,12 @@
import { Amounts } from "@gnu-taler/taler-util";
import { ProviderInfo, ProviderPaymentPaid, ProviderPaymentStatus, ProviderPaymentType } 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 BackupStatus { export interface BackupStatus {
deviceName: string; deviceName: string;
providers: ProviderInfo[] providers: ProviderInfo[];
sync: () => Promise<void>;
} }
function getStatusTypeOrder(t: ProviderPaymentStatus) { function getStatusTypeOrder(t: ProviderPaymentStatus) {
@ -40,7 +40,11 @@ export function useBackupStatus(): BackupStatus | undefined {
return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus) return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
}) })
setStatus({ deviceName: status.deviceId, providers }) async function sync() {
await wxApi.syncAllProviders()
}
setStatus({ deviceName: status.deviceId, providers, sync })
} }
run() run()
}, []) }, [])
@ -48,3 +52,4 @@ export function useBackupStatus(): BackupStatus | undefined {
return status return status
} }

View File

@ -0,0 +1,34 @@
import { ProviderInfo } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
export interface ProviderStatus {
info?: ProviderInfo;
sync: () => Promise<void>;
}
export function useProviderStatus(url: string): ProviderStatus | undefined {
const [status, setStatus] = useState<ProviderStatus | undefined>(undefined);
useEffect(() => {
async function run() {
//create a first list of backup info by currency
const status = await wxApi.getBackupInfo();
const providers = status.providers.filter(p => p.syncProviderBaseUrl === url);
const info = providers.length ? providers[0] : undefined;
async function sync() {
console.log("que tiene info", info)
if (info) {
await wxApi.syncOneProvider(info.syncProviderBaseUrl);
}
}
setStatus({ info, sync });
}
run();
}, []);
return status;
}

View File

@ -20,6 +20,7 @@
*/ */
import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
import { addDays } from 'date-fns';
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
import { BackupView as TestedComponent } from './BackupPage'; import { BackupView as TestedComponent } from './BackupPage';
@ -61,7 +62,27 @@ export const LotOfProviders = createExample(TestedComponent, {
"storageLimitInMegabytes": 16, "storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0" "supportedProtocolVersion": "0.0"
} }
}, { },{
"active": true,
"syncProviderBaseUrl": "http://sync.taler:9967/",
"lastSuccessfulBackupTimestamp": {
"t_ms": 1625063925078
},
"paymentProposalIds": [
"43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
],
"paymentStatus": {
"type": ProviderPaymentType.Paid,
"paidUntil": {
"t_ms": addDays(new Date(), 13).getTime()
}
},
"terms": {
"annualFee": "ARS:1",
"storageLimitInMegabytes": 16,
"supportedProtocolVersion": "0.0"
}
},{
"active": false, "active": false,
"syncProviderBaseUrl": "http://sync.demo.taler.net/", "syncProviderBaseUrl": "http://sync.demo.taler.net/",
"paymentProposalIds": [], "paymentProposalIds": [],

View File

@ -17,9 +17,9 @@
import { i18n, Timestamp } from "@gnu-taler/taler-util"; import { i18n, Timestamp } from "@gnu-taler/taler-util";
import { ProviderInfo } from "@gnu-taler/taler-wallet-core"; import { ProviderInfo } from "@gnu-taler/taler-wallet-core";
import { formatDuration, intervalToDuration } from "date-fns"; import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
import { JSX, VNode } from "preact"; import { Fragment, JSX, VNode } from "preact";
import { useBackupStatus } from "../hooks/useProvidersByCurrency"; import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "./popup"; import { Pages } from "./popup";
interface Props { interface Props {
@ -31,59 +31,59 @@ export function BackupPage({ onAddProvider }: Props): VNode {
if (!status) { if (!status) {
return <div>Loading...</div> return <div>Loading...</div>
} }
return <BackupView providers={status.providers} onAddProvider={onAddProvider} />; return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync}/>;
} }
export interface ViewProps { export interface ViewProps {
providers: ProviderInfo[], providers: ProviderInfo[],
onAddProvider: () => void; onAddProvider: () => void;
onSyncAll: () => Promise<void>;
} }
export function BackupView({ providers, onAddProvider }: ViewProps): VNode { export function BackupView({ providers, onAddProvider, onSyncAll }: 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: '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' }}>
{!!providers.length && <div> {!!providers.length && <div>
{providers.map((provider, idx) => { {providers.map((provider) => {
return <BackupLayout return <BackupLayout
status={provider.paymentStatus} status={provider.paymentStatus}
timestamp={provider.lastSuccessfulBackupTimestamp} timestamp={provider.lastSuccessfulBackupTimestamp}
id={idx} id={provider.syncProviderBaseUrl}
active={provider.active} active={provider.active}
subtitle={provider.syncProviderBaseUrl}
title={provider.syncProviderBaseUrl} title={provider.syncProviderBaseUrl}
/> />
})} })}
</div>} </div>}
{!providers.length && <div> {!providers.length && <div style={{ color: 'gray', fontWeight: 'bold', marginTop: 80, textAlign: 'center' }}>
There is not backup providers configured, add one with the button below <div>No backup providers configured</div>
<button class="pure-button button-success" style={{ marginTop: 15 }} onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></button>
</div>} </div>}
</section> </section>
<footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> {!!providers.length && <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}>
<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" disabled={!providers.length} style={{ marginLeft: 5 }} onClick={onAddProvider}>{ <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={onSyncAll}>{
providers.length > 1 ? providers.length > 1 ?
<i18n.Translate>sync all now</i18n.Translate>: <i18n.Translate>Sync all backups</i18n.Translate> :
<i18n.Translate>sync now</i18n.Translate> <i18n.Translate>Sync now</i18n.Translate>
}</button> }</button>
<button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>add provider</i18n.Translate></button> <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></button>
</div> </div>
</footer> </footer>}
</div> </div>
</div> </div>
) )
} }
interface TransactionLayoutProps { interface TransactionLayoutProps {
status?: any; status: any;
timestamp?: Timestamp; timestamp?: Timestamp;
title: string; title: string;
id: number; id: string;
subtitle?: string; active: boolean;
active?: boolean;
} }
function BackupLayout(props: TransactionLayoutProps): JSX.Element { function BackupLayout(props: TransactionLayoutProps): JSX.Element {
@ -107,13 +107,12 @@ 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 }}
> >
<div style={{ }}>
<div style={{ fontVariant: "small-caps", fontSize: "x-large" }}> <a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
<a href={Pages.provider_detail.replace(':pid', String(props.id))}><span>{props.title}</span></a>
</div> </div>
{dateStr && <div style={{ fontSize: "small" }}>Last time synced: {dateStr}</div>} {dateStr && <div style={{ fontSize: "small", marginTop: '0.5em' }}>Last synced: {dateStr}</div>}
{!dateStr && <div style={{ fontSize: "small", color: "red" }}>never synced</div>} {!dateStr && <div style={{ fontSize: "small", color: 'gray' }}>Not synced</div>}
</div> </div>
<div style={{ <div style={{
marginLeft: "auto", marginLeft: "auto",
@ -122,16 +121,32 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
alignItems: "center", alignItems: "center",
alignSelf: "center" alignSelf: "center"
}}> }}>
<div style={{ whiteSpace: 'nowrap' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
{!props.status ? "missing" : ( {
props.status?.type === 'paid' ? daysUntil(props.status.paidUntil) : 'unpaid' props.status?.type === 'paid' ?
)} <Fragment>
<div style={{ whiteSpace: 'nowrap', textAlign: 'center' }}>
Expires in
</div>
<div style={{ whiteSpace: 'nowrap', textAlign: 'center', fontWeight: 'bold', color: colorByTimeToExpire(props.status.paidUntil) }}>
{daysUntil(props.status.paidUntil)}
</div>
</Fragment>
:
'unpaid'
}
</div> </div>
</div> </div>
</div> </div>
); );
} }
function colorByTimeToExpire(d: Timestamp) {
if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
const months = differenceInMonths(d.t_ms, new Date())
return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
}
function daysUntil(d: Timestamp) { function daysUntil(d: Timestamp) {
if (d.t_ms === 'never') return undefined if (d.t_ms === 'never') return undefined
const duration = intervalToDuration({ const duration = intervalToDuration({
@ -150,5 +165,5 @@ function daysUntil(d: Timestamp) {
) )
] ]
}) })
return `${str} left` return `${str}`
} }

View File

@ -5,6 +5,7 @@ import * as wxApi from "../wxApi";
interface Props { interface Props {
currency: string; currency: string;
onBack: () => void;
} }
function getJsonIfOk(r: Response) { function getJsonIfOk(r: Response) {
@ -20,16 +21,14 @@ function getJsonIfOk(r: Response) {
} }
export function ProviderAddPage({ }: Props): VNode { export function ProviderAddPage({ onBack }: 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 [readingTerms, setReadingTerms] = useState<boolean | undefined>(undefined)
const alreadyCheckedTheTerms = readingTerms === false const alreadyCheckedTheTerms = readingTerms === false
if (!verifying) { if (!verifying) {
return <SetUrlView return <SetUrlView
onCancel={() => { onCancel={onBack}
setVerifying(undefined);
}}
onVerify={(url) => { onVerify={(url) => {
return fetch(`${url}/config`) return fetch(`${url}/config`)
.catch(e => { throw new Error(`Network error`) }) .catch(e => { throw new Error(`Network error`) })
@ -56,7 +55,7 @@ export function ProviderAddPage({ }: Props): VNode {
setReadingTerms(true) setReadingTerms(true)
}} }}
onConfirm={() => { onConfirm={() => {
wxApi.addBackupProvider(verifying.url).then(_ => history.go(-1)) wxApi.addBackupProvider(verifying.url).then(onBack)
}} }}
/> />

View File

@ -21,7 +21,8 @@ import { ContractTermsUtil } from "@gnu-taler/taler-wallet-core/src/util/contrac
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";
import { useBackupStatus } from "../hooks/useProvidersByCurrency"; import { useBackupStatus } from "../hooks/useBackupStatus";
import { useProviderStatus } from "../hooks/useProviderStatus.js";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
interface Props { interface Props {
@ -30,18 +31,16 @@ interface Props {
} }
export function ProviderDetailPage({ pid, onBack }: Props): VNode { export function ProviderDetailPage({ pid, onBack }: Props): VNode {
const status = useBackupStatus() const status = useProviderStatus(pid)
if (!status) { if (!status) {
return <div>Loading...</div> return <div>Loading...</div>
} }
const idx = parseInt(pid, 10) if (!status.info) {
if (Number.isNaN(idx) || !(status.providers[idx])) {
onBack() onBack()
return <div /> return <div />
} }
const info = status.providers[idx]; return <ProviderView info={status.info}
return <ProviderView info={info} onSync={status.sync}
onSync={() => { null }}
onDelete={() => { null }} onDelete={() => { null }}
onBack={onBack} onBack={onBack}
onExtend={() => { null }} onExtend={() => { null }}
@ -63,7 +62,7 @@ export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewP
<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" disabled 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" disabled style={{ marginLeft: 5 }} onClick={onExtend}><i18n.Translate>extend</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>} {info && <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>}
</div> </div>
</footer> </footer>
} }

View File

@ -103,13 +103,17 @@ function Application() {
onAddProvider={() => { onAddProvider={() => {
route(Pages.provider_add) 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_detail} component={ProviderDetailPage}
onBack={() => {
route(Pages.backup)
}}
/>
<Route path={Pages.provider_add} component={ProviderAddPage}
onBack={() => {
route(Pages.backup)
}}
/>
<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} />
</Router> </Router>

View File

@ -190,6 +190,15 @@ export function syncAllProviders(): Promise<void> {
return callBackend("runBackupCycle", {}) return callBackend("runBackupCycle", {})
} }
export function syncOneProvider(url: string): Promise<void> {
return callBackend("runBackupCycle", { providers: [url] })
}
export function removeProvider(url: string): Promise<void> {
return callBackend("removeBackupProvider", { provider: url })
}
export function extendedProvider(url: string): Promise<void> {
return callBackend("extendBackupProvider", { provider: url })
}
/** /**
* Retry a transaction * Retry a transaction