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())
.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 {
proposalId: string;
}
@ -791,7 +802,7 @@ export interface MakeSyncSignatureRequest {
/**
* Planchet for a coin during refresh.
*/
export interface RefreshPlanchetInfo {
export interface RefreshPlanchetInfo {
/**
* 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.
*/
interface ExchangeWithdrawDetails {
export interface ExchangeWithdrawDetails {
/**
* Exchange that the reserve will be created at.
*/

View File

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

View File

@ -119,7 +119,6 @@ export const decorators = [
margin: 0;
font-size: 100%;
padding: 0;
background-color: #f8faf7;
font-family: Arial, Helvetica, sans-serif;
}`}
</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`
max-width: 50%;
margin: auto;
height: 100%;
& h1:first-child {
margin-top: 0;
}
`
export const WalletActionOld = styled.section`
border: solid 5px black;
border-radius: 10px;
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;
zoom: 1;
line-height: normal;
@ -162,6 +172,7 @@ export const Button = styled.button`
cursor: pointer;
user-select: none;
box-sizing: border-box;
text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
font-family: inherit;
font-size: 100%;
@ -242,11 +253,11 @@ export const ButtonBoxPrimary = styled(ButtonBox)`
`
export const ButtonSuccess = styled(ButtonVariant)`
background-color: rgb(28, 184, 65);
background-color: #388e3c;
`
export const ButtonBoxSuccess = styled(ButtonBox)`
color: rgb(28, 184, 65);
border-color: rgb(28, 184, 65);
color: #388e3c;
border-color: #388e3c;
`
export const ButtonWarning = styled(ButtonVariant)`

View File

@ -136,7 +136,9 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayResult(res);
} catch (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)
*/
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 { View as TestedComponent } from './Withdraw';
@ -30,16 +33,29 @@ export default {
},
};
export const CompleteWithExchange = createExample(TestedComponent, {
export const WithdrawWithFee = createExample(TestedComponent, {
details: {
amount: 'USD:2',
possibleExchanges: [],
},
selectedExchange: 'Some exchange'
exchangeInfo: {
baseUrl: 'exchange.demo.taler.net'
} as ExchangeRecord,
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: {
amount: 'USD:2',
possibleExchanges: [],
},
exchangeInfo: {
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
*/
import { i18n } from '@gnu-taler/taler-util'
import { renderAmount } from "../renderHtml";
import { useState, useEffect } from "preact/hooks";
import {
acceptWithdrawal,
onUpdateNotification,
getWithdrawalDetailsForUri,
} from "../wxApi";
import { h } from 'preact';
import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util";
import { AmountLike, Amounts, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util';
import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
import { useEffect, useState } from "preact/hooks";
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 {
talerWithdrawUri?: string;
}
export interface ViewProps {
details: WithdrawUriInfoResponse;
selectedExchange?: string;
details: ExchangeWithdrawDetails;
amount: string;
accept: () => Promise<void>;
setCancelled: (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 (
<WalletAction>
<div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
<h1 style="font-family: monospace; font-size: 250%;">
<span style="color: #aa3939;"></span>Taler Wallet<span style="color: #aa3939;"></span>
</h1>
</div>
<div class="fade">
<WalletAction style={{ textAlign: 'center' }}>
<LogoHeader />
<h2>
{i18n.str`Digital cash withdrawal`}
</h2>
<section>
<div>
<h1><i18n.Translate>Digital Cash Withdrawal</i18n.Translate></h1>
<p><i18n.Translate>
You are about to withdraw{" "}
<strong>{renderAmount(details.amount)}</strong> from your bank account
into your wallet.
</i18n.Translate></p>
{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>
<Part title="Total to withdraw" text={amountToString(Amounts.sub(Amounts.parseOrThrow(amount), details.withdrawFee).amount)} kind='positive' />
<Part title="Chosen amount" text={amountToString(amount)} kind='neutral' />
{Amounts.isNonZero(details.withdrawFee) &&
<Part title="Exchange fee" text={amountToString(details.withdrawFee)} kind='negative' />
}
<Part title="Exchange" text={details.exchangeInfo.baseUrl} kind='neutral' big />
</div>
</div>
</section>
<section>
<div>
<ButtonSuccess
upperCased
disabled={!details.exchangeInfo.baseUrl}
onClick={accept}
>
{i18n.str`Accept fees and withdraw`}
</ButtonSuccess>
</div>
</section>
</WalletAction>
)
}
export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element {
const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined);
const [selectedExchange, setSelectedExchange] = useState<string | undefined>(undefined);
const [uriInfo, setUriInfo] = useState<WithdrawUriInfoResponse | undefined>(undefined);
const [details, setDetails] = useState<ExchangeWithdrawDetails | undefined>(undefined);
const [cancelled, setCancelled] = useState(false);
const [selecting, setSelecting] = useState(false);
const [error, setError] = useState<boolean>(false);
const [updateCounter, setUpdateCounter] = useState(1);
const [state, setState] = useState(1)
useEffect(() => {
return onUpdateNotification(() => {
@ -127,47 +107,59 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
const fetchData = async (): Promise<void> => {
try {
const res = await getWithdrawalDetailsForUri({ talerWithdrawUri });
setDetails(res);
if (res.defaultExchangeBaseUrl) {
setSelectedExchange(res.defaultExchangeBaseUrl);
}
setUriInfo(res);
} catch (e) {
console.error('error', JSON.stringify(e, undefined, 2))
setError(true)
}
};
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) {
return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>;
}
const accept = async (): Promise<void> => {
if (!selectedExchange) {
if (!details) {
throw Error("can't accept, no exchange selected");
}
console.log("accepting exchange", selectedExchange);
const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange);
console.log("accepting exchange", details.exchangeInfo.baseUrl);
const res = await acceptWithdrawal(talerWithdrawUri, details.exchangeInfo.baseUrl);
console.log("accept withdrawal response", res);
if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
}
};
if (!details) {
return <span><i18n.Translate>Loading...</i18n.Translate></span>;
}
if (cancelled) {
return <span><i18n.Translate>Withdraw operation has been cancelled.</i18n.Translate></span>;
}
if (error) {
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}
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 { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallLightText, WalletBox } from "../components/styled";
import { ErrorMessage } from "../components/ErrorMessage";
import { Part } from "../components/Part";
export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
const [transaction, setTransaction] = useState<
@ -60,7 +61,6 @@ export interface WalletTransactionProps {
onBack: () => void,
}
export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
function Status() {
@ -90,16 +90,6 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall
</footer>
</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) {
const aj = Amounts.jsonifyAmount(text)

View File

@ -38,9 +38,11 @@ import {
DeleteTransactionRequest,
RetryTransactionRequest,
SetWalletDeviceIdRequest,
GetExchangeWithdrawalInfo,
} from "@gnu-taler/taler-util";
import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } 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 {
newValue: boolean;
@ -281,6 +283,16 @@ export function getWithdrawalDetailsForUri(
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> {
return callBackend("prepareTip", req);
}