first approach to new design for withdraw

This commit is contained in:
Sebastian 2021-09-08 15:30:32 -03:00
parent a72ec5971e
commit 217f34397f
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
11 changed files with 161 additions and 107 deletions

View File

@ -713,6 +713,17 @@ export const codecForGetWithdrawalDetailsForUri = (): Codec<GetWithdrawalDetails
.property("talerWithdrawUri", codecForString()) .property("talerWithdrawUri", codecForString())
.build("GetWithdrawalDetailsForUriRequest"); .build("GetWithdrawalDetailsForUriRequest");
export interface GetExchangeWithdrawalInfo {
exchangeBaseUrl: string;
amount: AmountJson;
}
export const codecForGetExchangeWithdrawalInfo = (): Codec<GetExchangeWithdrawalInfo> =>
buildCodecForObject<GetExchangeWithdrawalInfo>()
.property("exchangeBaseUrl", codecForString())
.property("amount", codecForAmountJson())
.build("GetExchangeWithdrawalInfo");
export interface AbortProposalRequest { export interface AbortProposalRequest {
proposalId: string; proposalId: string;
} }
@ -791,7 +802,7 @@ export interface MakeSyncSignatureRequest {
/** /**
* Planchet for a coin during refresh. * Planchet for a coin during refresh.
*/ */
export interface RefreshPlanchetInfo { export interface RefreshPlanchetInfo {
/** /**
* Public key for the coin. * Public key for the coin.
*/ */

View File

@ -92,7 +92,7 @@ interface DenominationSelectionInfo {
* *
* Sent to the wallet frontend to be rendered and shown to the user. * Sent to the wallet frontend to be rendered and shown to the user.
*/ */
interface ExchangeWithdrawDetails { export interface ExchangeWithdrawDetails {
/** /**
* Exchange that the reserve will be created at. * Exchange that the reserve will be created at.
*/ */

View File

@ -28,6 +28,7 @@ import {
codecForDeleteTransactionRequest, codecForDeleteTransactionRequest,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
codecForSetWalletDeviceIdRequest, codecForSetWalletDeviceIdRequest,
codecForGetExchangeWithdrawalInfo,
durationFromSpec, durationFromSpec,
durationMin, durationMin,
getDurationRemaining, getDurationRemaining,
@ -693,6 +694,10 @@ async function dispatchRequestInternal(
const req = codecForGetWithdrawalDetailsForUri().decode(payload); const req = codecForGetWithdrawalDetailsForUri().decode(payload);
return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri); return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
} }
case "getExchangeWithdrawalInfo": {
const req = codecForGetExchangeWithdrawalInfo().decode(payload);
return await getExchangeWithdrawalInfo(ws, req.exchangeBaseUrl, req.amount);
}
case "acceptManualWithdrawal": { case "acceptManualWithdrawal": {
const req = codecForAcceptManualWithdrawalRequet().decode(payload); const req = codecForAcceptManualWithdrawalRequet().decode(payload);
const res = await acceptManualWithdrawal( const res = await acceptManualWithdrawal(

View File

@ -119,7 +119,6 @@ export const decorators = [
margin: 0; margin: 0;
font-size: 100%; font-size: 100%;
padding: 0; padding: 0;
background-color: #f8faf7;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
}`} }`}
</style> </style>

View File

@ -0,0 +1,16 @@
import { AmountLike } from "@gnu-taler/taler-util";
import { ExtraLargeText, LargeText, SmallLightText } from "./styled";
export type Kind = 'positive' | 'negative' | 'neutral';
interface Props {
title: string, text: AmountLike, kind: Kind, big?: boolean
}
export function Part({ text, title, kind, big }: Props) {
const Text = big ? ExtraLargeText : LargeText;
return <div style={{ margin: '1em' }}>
<SmallLightText style={{ margin: '.5em' }}>{title}</SmallLightText>
<Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}>
{text}
</Text>
</div>
}

View File

@ -12,6 +12,16 @@ export const PaymentStatus = styled.div<{ color: string }>`
` `
export const WalletAction = styled.section` export const WalletAction = styled.section`
max-width: 50%;
margin: auto;
height: 100%;
& h1:first-child {
margin-top: 0;
}
`
export const WalletActionOld = styled.section`
border: solid 5px black; border: solid 5px black;
border-radius: 10px; border-radius: 10px;
margin-left: auto; margin-left: auto;
@ -152,7 +162,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
` `
export const Button = styled.button` export const Button = styled.button<{ upperCased?: boolean }>`
display: inline-block; display: inline-block;
zoom: 1; zoom: 1;
line-height: normal; line-height: normal;
@ -162,6 +172,7 @@ export const Button = styled.button`
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
font-family: inherit; font-family: inherit;
font-size: 100%; font-size: 100%;
@ -242,11 +253,11 @@ export const ButtonBoxPrimary = styled(ButtonBox)`
` `
export const ButtonSuccess = styled(ButtonVariant)` export const ButtonSuccess = styled(ButtonVariant)`
background-color: rgb(28, 184, 65); background-color: #388e3c;
` `
export const ButtonBoxSuccess = styled(ButtonBox)` export const ButtonBoxSuccess = styled(ButtonBox)`
color: rgb(28, 184, 65); color: #388e3c;
border-color: rgb(28, 184, 65); border-color: #388e3c;
` `
export const ButtonWarning = styled(ButtonVariant)` export const ButtonWarning = styled(ButtonVariant)`

View File

@ -136,7 +136,9 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayResult(res); setPayResult(res);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setPayErrMsg(e.message); if (e instanceof Error) {
setPayErrMsg(e.message);
}
} }
} }

View File

@ -19,6 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { amountFractionalBase, Amounts } from '@gnu-taler/taler-util';
import { ExchangeRecord } from '@gnu-taler/taler-wallet-core';
import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
import { createExample } from '../test-utils'; import { createExample } from '../test-utils';
import { View as TestedComponent } from './Withdraw'; import { View as TestedComponent } from './Withdraw';
@ -30,16 +33,29 @@ export default {
}, },
}; };
export const CompleteWithExchange = createExample(TestedComponent, { export const WithdrawWithFee = createExample(TestedComponent, {
details: { details: {
amount: 'USD:2', exchangeInfo: {
possibleExchanges: [], baseUrl: 'exchange.demo.taler.net'
}, } as ExchangeRecord,
selectedExchange: 'Some exchange' withdrawFee: {
currency: 'USD',
fraction: amountFractionalBase*0.5,
value: 0
},
} as ExchangeWithdrawDetails,
amount: 'USD:2',
}) })
export const CompleteWithoutExchange = createExample(TestedComponent, { export const WithdrawWithoutFee = createExample(TestedComponent, {
details: { details: {
amount: 'USD:2', exchangeInfo: {
possibleExchanges: [], baseUrl: 'exchange.demo.taler.net'
}, } as ExchangeRecord,
withdrawFee: {
currency: 'USD',
fraction: 0,
value: 0
},
} as ExchangeWithdrawDetails,
amount: 'USD:2',
}) })

View File

@ -21,98 +21,78 @@
* @author Florian Dold * @author Florian Dold
*/ */
import { i18n } from '@gnu-taler/taler-util' import { AmountLike, Amounts, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util';
import { renderAmount } from "../renderHtml"; import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
import { useEffect, useState } from "preact/hooks";
import { useState, useEffect } from "preact/hooks";
import {
acceptWithdrawal,
onUpdateNotification,
getWithdrawalDetailsForUri,
} from "../wxApi";
import { h } from 'preact';
import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util";
import { JSX } from "preact/jsx-runtime"; import { JSX } from "preact/jsx-runtime";
import { WalletAction } from '../components/styled'; import { LogoHeader } from '../components/LogoHeader';
import { Part } from '../components/Part';
import { ButtonSuccess, WalletAction } from '../components/styled';
import {
acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, onUpdateNotification
} from "../wxApi";
interface Props { interface Props {
talerWithdrawUri?: string; talerWithdrawUri?: string;
} }
export interface ViewProps { export interface ViewProps {
details: WithdrawUriInfoResponse; details: ExchangeWithdrawDetails;
selectedExchange?: string; amount: string;
accept: () => Promise<void>; accept: () => Promise<void>;
setCancelled: (b: boolean) => void; setCancelled: (b: boolean) => void;
setSelecting: (b: boolean) => void; setSelecting: (b: boolean) => void;
}; };
export function View({ details, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { function amountToString(text: AmountLike) {
const aj = Amounts.jsonifyAmount(text)
const amount = Amounts.stringifyValue(aj)
return `${amount} ${aj.currency}`
}
export function View({ details, amount, accept, setCancelled, setSelecting }: ViewProps) {
return ( return (
<WalletAction> <WalletAction style={{ textAlign: 'center' }}>
<div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;"> <LogoHeader />
<h1 style="font-family: monospace; font-size: 250%;"> <h2>
<span style="color: #aa3939;"></span>Taler Wallet<span style="color: #aa3939;"></span> {i18n.str`Digital cash withdrawal`}
</h1> </h2>
</div> <section>
<div class="fade">
<div> <div>
<h1><i18n.Translate>Digital Cash Withdrawal</i18n.Translate></h1> <Part title="Total to withdraw" text={amountToString(Amounts.sub(Amounts.parseOrThrow(amount), details.withdrawFee).amount)} kind='positive' />
<p><i18n.Translate> <Part title="Chosen amount" text={amountToString(amount)} kind='neutral' />
You are about to withdraw{" "} {Amounts.isNonZero(details.withdrawFee) &&
<strong>{renderAmount(details.amount)}</strong> from your bank account <Part title="Exchange fee" text={amountToString(details.withdrawFee)} kind='negative' />
into your wallet. }
</i18n.Translate></p> <Part title="Exchange" text={details.exchangeInfo.baseUrl} kind='neutral' big />
{selectedExchange ? (
<p><i18n.Translate>
The exchange <strong>{selectedExchange}</strong> will be used as the
Taler payment service provider.
</i18n.Translate></p>
) : null}
<div>
<button
class="pure-button button-success"
disabled={!selectedExchange}
onClick={() => accept()}
>
{i18n.str`Accept fees and withdraw`}
</button>
<p>
<span
role="button"
tabIndex={0}
style={{ textDecoration: "underline", cursor: "pointer" }}
onClick={() => setSelecting(true)}
>
{i18n.str`Chose different exchange provider`}
</span>
<br />
<span
role="button"
tabIndex={0}
style={{ textDecoration: "underline", cursor: "pointer" }}
onClick={() => setCancelled(true)}
>
{i18n.str`Cancel withdraw operation`}
</span>
</p>
</div>
</div> </div>
</div> </section>
<section>
<div>
<ButtonSuccess
upperCased
disabled={!details.exchangeInfo.baseUrl}
onClick={accept}
>
{i18n.str`Accept fees and withdraw`}
</ButtonSuccess>
</div>
</section>
</WalletAction> </WalletAction>
) )
} }
export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element { export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element {
const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined); const [uriInfo, setUriInfo] = useState<WithdrawUriInfoResponse | undefined>(undefined);
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined); const [details, setDetails] = useState<ExchangeWithdrawDetails | undefined>(undefined);
const [cancelled, setCancelled] = useState(false); const [cancelled, setCancelled] = useState(false);
const [selecting, setSelecting] = useState(false); const [selecting, setSelecting] = useState(false);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [updateCounter, setUpdateCounter] = useState(1); const [updateCounter, setUpdateCounter] = useState(1);
const [state, setState] = useState(1)
useEffect(() => { useEffect(() => {
return onUpdateNotification(() => { return onUpdateNotification(() => {
@ -127,47 +107,59 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
try { try {
const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); const res = await getWithdrawalDetailsForUri({ talerWithdrawUri });
setDetails(res); setUriInfo(res);
if (res.defaultExchangeBaseUrl) {
setSelectedExchange(res.defaultExchangeBaseUrl);
}
} catch (e) { } catch (e) {
console.error('error', JSON.stringify(e, undefined, 2)) console.error('error', JSON.stringify(e, undefined, 2))
setError(true) setError(true)
} }
}; };
fetchData(); fetchData();
}, [selectedExchange, selecting, talerWithdrawUri, updateCounter, state]); }, [selecting, talerWithdrawUri, updateCounter]);
useEffect(() => {
async function fetchData() {
if (!uriInfo || !uriInfo.defaultExchangeBaseUrl) return
const res = await getExchangeWithdrawalInfo({
exchangeBaseUrl: uriInfo.defaultExchangeBaseUrl,
amount: Amounts.parseOrThrow(uriInfo.amount)
})
setDetails(res)
}
fetchData()
}, [uriInfo])
if (!talerWithdrawUri) { if (!talerWithdrawUri) {
return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>;
} }
const accept = async (): Promise<void> => { const accept = async (): Promise<void> => {
if (!selectedExchange) { if (!details) {
throw Error("can't accept, no exchange selected"); throw Error("can't accept, no exchange selected");
} }
console.log("accepting exchange", selectedExchange); console.log("accepting exchange", details.exchangeInfo.baseUrl);
const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange); const res = await acceptWithdrawal(talerWithdrawUri, details.exchangeInfo.baseUrl);
console.log("accept withdrawal response", res); console.log("accept withdrawal response", res);
if (res.confirmTransferUrl) { if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl; document.location.href = res.confirmTransferUrl;
} }
}; };
if (!details) {
return <span><i18n.Translate>Loading...</i18n.Translate></span>;
}
if (cancelled) { if (cancelled) {
return <span><i18n.Translate>Withdraw operation has been cancelled.</i18n.Translate></span>; return <span><i18n.Translate>Withdraw operation has been cancelled.</i18n.Translate></span>;
} }
if (error) { if (error) {
return <span><i18n.Translate>This URI is not valid anymore.</i18n.Translate></span>; return <span><i18n.Translate>This URI is not valid anymore.</i18n.Translate></span>;
} }
if (!uriInfo) {
return <span><i18n.Translate>Loading...</i18n.Translate></span>;
}
if (!details) {
return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>;
}
return <View accept={accept} return <View accept={accept}
setCancelled={setCancelled} setSelecting={setSelecting} setCancelled={setCancelled} setSelecting={setSelecting}
details={details} selectedExchange={selectedExchange} details={details} amount={uriInfo.amount}
/> />
} }

View File

@ -24,6 +24,7 @@ import { Pages } from "../NavigationBar";
import emptyImg from "../../static/img/empty.png" import emptyImg from "../../static/img/empty.png"
import { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallLightText, WalletBox } from "../components/styled"; import { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallLightText, WalletBox } from "../components/styled";
import { ErrorMessage } from "../components/ErrorMessage"; import { ErrorMessage } from "../components/ErrorMessage";
import { Part } from "../components/Part";
export function TransactionPage({ tid }: { tid: string; }): JSX.Element { export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
const [transaction, setTransaction] = useState< const [transaction, setTransaction] = useState<
@ -60,7 +61,6 @@ export interface WalletTransactionProps {
onBack: () => void, onBack: () => void,
} }
export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) { export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
function Status() { function Status() {
@ -90,16 +90,6 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall
</footer> </footer>
</WalletBox> </WalletBox>
} }
type Kind = 'positive' | 'negative' | 'neutral';
function Part({ text, title, kind, big }: { title: string, text: AmountLike, kind: Kind, big?: boolean }) {
const Text = big ? ExtraLargeText : LargeText;
return <div style={{ margin: '1em' }}>
<SmallLightText style={{ margin: '.5em' }}>{title}</SmallLightText>
<Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}>
{text}
</Text>
</div>
}
function amountToString(text: AmountLike) { function amountToString(text: AmountLike) {
const aj = Amounts.jsonifyAmount(text) const aj = Amounts.jsonifyAmount(text)

View File

@ -38,9 +38,11 @@ import {
DeleteTransactionRequest, DeleteTransactionRequest,
RetryTransactionRequest, RetryTransactionRequest,
SetWalletDeviceIdRequest, SetWalletDeviceIdRequest,
GetExchangeWithdrawalInfo,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core"; import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core";
import { BackupInfo } from "@gnu-taler/taler-wallet-core"; import { BackupInfo } from "@gnu-taler/taler-wallet-core";
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
export interface ExtendedPermissionsResponse { export interface ExtendedPermissionsResponse {
newValue: boolean; newValue: boolean;
@ -281,6 +283,16 @@ export function getWithdrawalDetailsForUri(
return callBackend("getWithdrawalDetailsForUri", req); return callBackend("getWithdrawalDetailsForUri", req);
} }
/**
* Get diagnostics information
*/
export function getExchangeWithdrawalInfo(
req: GetExchangeWithdrawalInfo,
): Promise<ExchangeWithdrawDetails> {
return callBackend("getExchangeWithdrawalInfo", req);
}
export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> { export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
return callBackend("prepareTip", req); return callBackend("prepareTip", req);
} }