manual withdrawal process

This commit is contained in:
Sebastian 2021-09-20 14:05:40 -03:00
parent 9a0285ee4b
commit 8cde98947b
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
15 changed files with 380 additions and 29 deletions

View File

@ -33,6 +33,7 @@ import { PopupNavigation } from './components/styled'
export enum Pages {
welcome = '/welcome',
balance = '/balance',
manual_withdraw = '/manual-withdraw',
settings = '/settings',
dev = '/dev',
backup = '/backup',

View File

@ -486,6 +486,33 @@ export const Input = styled.div<{ invalid?: boolean }>`
}
`
export const InputWithLabel = styled.div<{ invalid?: boolean }>`
& label {
display: block;
padding: 5px;
color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
}
& > div {
position: relative;
display: flex;
top: 0px;
bottom: 0px;
& > div {
position: absolute;
background-color: lightgray;
padding: 5px;
margin: 2px;
}
& > input {
flex: 1;
padding: 5px;
border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
}
}
`
export const ErrorBox = styled.div`
border: 2px solid #f5c6cb;
border-radius: 0.25em;

View File

@ -141,7 +141,7 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview,
}
<section>
{terms.status === 'new' && !accepted &&
{terms.status === 'new' && !accepted && !reviewing &&
<ButtonSuccess
upperCased
disabled={!details.exchangeInfo.baseUrl}

View File

@ -20,20 +20,21 @@ import {
i18n
} from "@gnu-taler/taler-util";
import { JSX, h } from "preact";
import { PopupBox, Centered } from "../components/styled/index";
import { PopupBox, Centered, ButtonPrimary } from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
export function BalancePage() {
export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
const balance = useBalances()
return <BalanceView balance={balance} Linker={PageLink} />
return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
}
export interface BalanceViewProps {
balance: BalancesHook,
Linker: typeof PageLink,
balance: BalancesHook;
Linker: typeof PageLink;
goToWalletManualWithdraw: () => void;
}
export function BalanceView({ balance, Linker }: BalanceViewProps) {
export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
if (!balance) {
return <span />
}
@ -57,7 +58,9 @@ export function BalanceView({ balance, Linker }: BalanceViewProps) {
</i18n.Translate></p>
)
}
return <ShowBalances wallet={balance.response} />
return <ShowBalances wallet={balance.response}
onWithdraw={goToWalletManualWithdraw}
/>
}
function formatPending(entry: Balance): JSX.Element {
@ -96,7 +99,7 @@ function formatPending(entry: Balance): JSX.Element {
}
function ShowBalances({ wallet }: { wallet: BalancesResponse }) {
function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) {
return <PopupBox>
<section>
<Centered>{wallet.balances.map((entry) => {
@ -113,5 +116,9 @@ function ShowBalances({ wallet }: { wallet: BalancesResponse }) {
);
})}</Centered>
</section>
<footer>
<div />
<ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary>
</footer>
</PopupBox>
}

View File

@ -22,8 +22,8 @@
import { setupI18n } from "@gnu-taler/taler-util";
import { createHashHistory } from "history";
import { render, h } from "preact";
import Router, { route, Route } from "preact-router";
import { render, h, VNode } from "preact";
import Router, { route, Route, getCurrentUrl } from "preact-router";
import { useEffect, useState } from "preact/hooks";
import { DevContextProvider } from "./context/devContext";
import { useTalerActionURL } from "./hooks/useTalerActionURL";
@ -96,9 +96,16 @@ function Application() {
<WalletNavBar />
<div style={{ width: 400, height: 290 }}>
<Router history={createHashHistory()}>
<Route path={Pages.balance} component={BalancePage} />
<Route path={Pages.balance} component={BalancePage}
goToWalletManualWithdraw={() => goToWalletPage(Pages.manual_withdraw)}
/>
<Route path={Pages.settings} component={SettingsPage} />
<Route path={Pages.dev} component={DeveloperPage} />
<Route path={Pages.transaction}
component={({ tid }: { tid: string }) => goToWalletPage(Pages.transaction.replace(':tid', tid))}
/>
<Route path={Pages.history} component={HistoryPage} />
<Route path={Pages.backup} component={BackupPage}
onAddProvider={() => {
@ -123,6 +130,13 @@ function Application() {
);
}
function goToWalletPage(page: Pages | string): null {
chrome.tabs.create({
active: true,
url: chrome.extension.getURL(`/static/wallet.html#${page}`),
})
return null
}
function Redirect({ to }: { to: string }): null {
useEffect(() => {

View File

@ -57,7 +57,7 @@ export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps):
title={provider.name}
/>
)}
{!providers.length && <Centered style={{marginTop: 100}}>
{!providers.length && <Centered style={{ marginTop: 100 }}>
<BoldLight>No backup providers configured</BoldLight>
<ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
</Centered>}
@ -98,8 +98,8 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element {
<div style={{ color: !props.active ? "grey" : undefined }}>
<a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
{dateStr && <SmallText style={{marginTop: 5}}>Last synced: {dateStr}</SmallText>}
{!dateStr && <SmallLightText style={{marginTop: 5}}>Not synced</SmallLightText>}
{dateStr && <SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>}
{!dateStr && <SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>}
</div>
<div>
{props.status?.type === 'paid' ?

View File

@ -19,21 +19,24 @@ import {
Balance, BalancesResponse,
i18n
} from "@gnu-taler/taler-util";
import { JSX, h } from "preact";
import { WalletBox, Centered } from "../components/styled/index";
import { JSX } from "preact";
import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index";
import { BalancesHook, useBalances } from "../hooks/useBalances";
import { PageLink, renderAmount } from "../renderHtml";
export function BalancePage() {
export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
const balance = useBalances()
return <BalanceView balance={balance} Linker={PageLink} />
return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
}
export interface BalanceViewProps {
balance: BalancesHook,
Linker: typeof PageLink,
balance: BalancesHook;
Linker: typeof PageLink;
goToWalletManualWithdraw: () => void;
}
export function BalanceView({ balance, Linker }: BalanceViewProps) {
export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
if (!balance) {
return <span />
}
@ -57,7 +60,9 @@ export function BalanceView({ balance, Linker }: BalanceViewProps) {
</i18n.Translate></p>
)
}
return <ShowBalances wallet={balance.response} />
return <ShowBalances wallet={balance.response}
onWithdraw={goToWalletManualWithdraw}
/>
}
function formatPending(entry: Balance): JSX.Element {
@ -96,7 +101,7 @@ function formatPending(entry: Balance): JSX.Element {
}
function ShowBalances({ wallet }: { wallet: BalancesResponse }) {
function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) {
return <WalletBox>
<section>
<Centered>{wallet.balances.map((entry) => {
@ -113,5 +118,9 @@ function ShowBalances({ wallet }: { wallet: BalancesResponse }) {
);
})}</Centered>
</section>
<footer>
<div />
<ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary>
</footer>
</WalletBox>
}

View File

@ -0,0 +1,56 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { CreateManualWithdraw as TestedComponent } from './CreateManualWithdraw';
export default {
title: 'wallet/manual withdraw/creation',
component: TestedComponent,
argTypes: {
}
};
export const InitialState = createExample(TestedComponent, {
});
export const WithExchangeFilled = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
});
export const WithExchangeAndAmountFilled = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
initialAmount: '10'
});
export const WithExchangeError = createExample(TestedComponent, {
initialExchange: 'http://exchange.tal',
error: 'The exchange url seems invalid'
});
export const WithAmountError = createExample(TestedComponent, {
currency: 'COL',
initialExchange: 'http://exchange.taler:8081',
initialAmount: 'e'
});

View File

@ -0,0 +1,57 @@
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage";
import { ButtonPrimary, Input, InputWithLabel, LightText, WalletBox } from "../components/styled";
export interface Props {
error: string | undefined;
currency: string | undefined;
initialExchange?: string;
initialAmount?: string;
onExchangeChange: (exchange: string) => void;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
}
export function CreateManualWithdraw({ onExchangeChange, initialExchange, initialAmount, error, currency, onCreate }: Props): VNode {
const [exchange, setExchange] = useState(initialExchange || "");
const [amount, setAmount] = useState(initialAmount || "");
const parsedAmount = Amounts.parse(`${currency}:${amount}`)
let timeout = useRef<number | undefined>(undefined);
useEffect(() => {
if (timeout) window.clearTimeout(timeout.current)
timeout.current = window.setTimeout(async () => {
onExchangeChange(exchange)
}, 1000);
}, [exchange])
return (
<WalletBox>
<section>
<ErrorMessage title={error && "Can't create the reserve"} description={error} />
<h2>Manual Withdrawal</h2>
<LightText>Choose a exchange to create a reserve and then fill the reserve to withdraw the coins</LightText>
<p>
<Input invalid={!!exchange && !currency}>
<label>Exchange</label>
<input type="text" placeholder="https://" value={exchange} onChange={(e) => setExchange(e.currentTarget.value)} />
<small>http://exchange.taler:8081</small>
</Input>
{currency && <InputWithLabel invalid={!!amount && !parsedAmount}>
<label>Amount</label>
<div>
<div>{currency}</div>
<input type="number" style={{ paddingLeft: `${currency.length}em` }} value={amount} onChange={e => setAmount(e.currentTarget.value)} />
</div>
</InputWithLabel>}
</p>
</section>
<footer>
<div />
<ButtonPrimary disabled={!parsedAmount || !exchange} onClick={() => onCreate(exchange, parsedAmount!)}>Create</ButtonPrimary>
</footer>
</WalletBox>
);
}

View File

@ -0,0 +1,71 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { CreateManualWithdraw } from "./CreateManualWithdraw";
import * as wxApi from '../wxApi'
import { AcceptManualWithdrawalResult, AmountJson, Amounts } from "@gnu-taler/taler-util";
import { ReserveCreated } from "./ReserveCreated.js";
interface Props {
}
export function ManualWithdrawPage({ }: Props): VNode {
const [success, setSuccess] = useState<AcceptManualWithdrawalResult | undefined>(undefined)
const [currency, setCurrency] = useState<string | undefined>(undefined)
const [error, setError] = useState<string | undefined>(undefined)
async function onExchangeChange(exchange: string | undefined) {
if (!exchange) return
try {
const r = await fetch(`${exchange}/keys`)
const j = await r.json()
setCurrency(j.currency)
} catch (e) {
setError('The exchange url seems invalid')
setCurrency(undefined)
}
}
async function doCreate(exchangeBaseUrl: string, amount: AmountJson) {
try {
const resp = await wxApi.acceptManualWithdrawal(exchangeBaseUrl, Amounts.stringify(amount))
setSuccess(resp)
} catch (e) {
if (e instanceof Error) {
setError(e.message)
} else {
setError('unexpected error')
}
setSuccess(undefined)
}
}
if (success) {
return <ReserveCreated reservePub={success.reservePub} paytos={success.exchangePaytoUris} onBack={() => {}}/>
}
return <CreateManualWithdraw
error={error} currency={currency}
onCreate={doCreate} onExchangeChange={onExchangeChange}
/>;
}

View File

@ -0,0 +1,40 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from '../test-utils';
import { ReserveCreated as TestedComponent } from './ReserveCreated';
export default {
title: 'wallet/manual withdraw/reserve created',
component: TestedComponent,
argTypes: {
}
};
export const InitialState = createExample(TestedComponent, {
reservePub: 'ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ',
paytos: [
'payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG',
'payto://x-taler-bank/international-bank.com/myaccount?amount=COL%3A1&message=Taler+Withdrawal+TYQTE7VA4M9GZQ4TR06YBNGA05AJGMFNSK4Q62NXR2FKNDB1J4EX',
]
});

View File

@ -0,0 +1,41 @@
import { Fragment, VNode } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../components/QR";
import { ButtonBox, FontIcon, WalletBox } from "../components/styled";
export interface Props {
reservePub: string;
paytos: string[];
onBack: () => void;
}
export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode {
const [opened, setOpened] = useState(-1)
return (
<WalletBox>
<section>
<h2>Reserve created!</h2>
<p>Now you need to send money to the exchange to one of the following accounts</p>
<p>To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.</p>
</section>
<section>
<ul>
{paytos.map((href, idx) => {
const url = new URL(href)
return <li key={idx}><p>
<a href="" onClick={(e) => { setOpened(o => o === idx ? -1 : idx); e.preventDefault() }}>{url.pathname}</a>
{opened === idx && <Fragment>
<p>If your system supports RFC 8905, you can do this by opening <a href={href}>this URI</a> or scan the QR with your wallet</p>
<QR text={href} />
</Fragment>}
</p></li>
})}
</ul>
</section>
<footer>
<ButtonBox onClick={onBack}><FontIcon>&#x2190;</FontIcon></ButtonBox>
<div />
</footer>
</WalletBox>
);
}

View File

@ -51,7 +51,7 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
transaction={transaction}
onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))}
onBack={() => { history.go(-1); }} />;
onBack={() => { route(Pages.history) }} />;
}
export interface WalletTransactionProps {

View File

@ -41,6 +41,8 @@ import { SettingsPage } from "./wallet/Settings";
import { TransactionPage } from './wallet/Transaction';
import { WelcomePage } from "./wallet/Welcome";
import { BackupPage } from './wallet/BackupPage';
import { DeveloperPage } from "./popup/Debug.js";
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js";
function main(): void {
@ -52,7 +54,9 @@ function main(): void {
render(<Application />, container);
} catch (e) {
console.error("got error", e);
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
}
@ -65,10 +69,10 @@ if (document.readyState === "loading") {
}
function withLogoAndNavBar(Component: any) {
return () => <Fragment>
return (props: any) => <Fragment>
<LogoHeader />
<WalletNavBar />
<Component />
<Component {...props} />
</Fragment>
}
@ -81,14 +85,20 @@ function Application() {
<Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} />
<Route path={Pages.transaction} component={withLogoAndNavBar(TransactionPage)} />
<Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)} />
<Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)}
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
/>
<Route path={Pages.settings} component={withLogoAndNavBar(SettingsPage)} />
<Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} />
<Route path={Pages.manual_withdraw} component={withLogoAndNavBar(ManualWithdrawPage)} />
<Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} />
<Route path={Pages.payback} component={() => <div>no yet implemented</div>} />
<Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} />
<Route path={Pages.dev} component={withLogoAndNavBar(DeveloperPage)} />
{/** call to action */}
<Route path={Pages.pay} component={PayPage} />
<Route path={Pages.refund} component={RefundPage} />

View File

@ -40,6 +40,9 @@ import {
SetWalletDeviceIdRequest,
GetExchangeWithdrawalInfo,
AcceptExchangeTosRequest,
AcceptManualWithdrawalResult,
AcceptManualWithdrawalRequest,
AmountJson,
} from "@gnu-taler/taler-util";
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core";
import { BackupInfo } from "@gnu-taler/taler-wallet-core";
@ -252,6 +255,21 @@ export function acceptWithdrawal(
});
}
/**
* Create a reserve into the exchange that expect the amount indicated
* @param exchangeBaseUrl
* @param amount
* @returns
*/
export function acceptManualWithdrawal(
exchangeBaseUrl: string,
amount: string,
): Promise<AcceptManualWithdrawalResult> {
return callBackend("acceptManualWithdrawal", {
amount, exchangeBaseUrl
});
}
export function setExchangeTosAccepted(
exchangeBaseUrl: string,
etag: string | undefined