add more backup stories, sync by one provider
This commit is contained in:
parent
ba995882ba
commit
655c5fc18a
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
@ -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": [],
|
||||||
|
@ -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}`
|
||||||
}
|
}
|
@ -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)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user