This commit is contained in:
Sebastian 2021-09-17 15:48:33 -03:00
parent 490620ad04
commit 315b167bee
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
12 changed files with 231 additions and 139 deletions

View File

@ -22,6 +22,7 @@ export interface PayUriResult {
orderId: string; orderId: string;
sessionId: string; sessionId: string;
claimToken: string | undefined; claimToken: string | undefined;
noncePriv: string | undefined;
} }
export interface WithdrawUriResult { export interface WithdrawUriResult {
@ -147,6 +148,7 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const c = pi?.rest.split("?"); const c = pi?.rest.split("?");
const q = new URLSearchParams(c[1] ?? ""); const q = new URLSearchParams(c[1] ?? "");
const claimToken = q.get("c") ?? undefined; const claimToken = q.get("c") ?? undefined;
const noncePriv = q.get("n") ?? undefined;
const parts = c[0].split("/"); const parts = c[0].split("/");
if (parts.length < 3) { if (parts.length < 3) {
return undefined; return undefined;
@ -163,6 +165,7 @@ export function parsePayUri(s: string): PayUriResult | undefined {
orderId, orderId,
sessionId: sessionId, sessionId: sessionId,
claimToken, claimToken,
noncePriv,
}; };
} }

View File

@ -325,6 +325,7 @@ export const codecForPreparePayResultPaymentPossible = (): Codec<PreparePayResul
.property("contractTerms", codecForContractTerms()) .property("contractTerms", codecForContractTerms())
.property("proposalId", codecForString()) .property("proposalId", codecForString())
.property("contractTermsHash", codecForString()) .property("contractTermsHash", codecForString())
.property("noncePriv", codecForString())
.property( .property(
"status", "status",
codecForConstString(PreparePayResultType.PaymentPossible), codecForConstString(PreparePayResultType.PaymentPossible),
@ -336,6 +337,7 @@ export const codecForPreparePayResultInsufficientBalance = (): Codec<PreparePayR
.property("amountRaw", codecForAmountString()) .property("amountRaw", codecForAmountString())
.property("contractTerms", codecForAny()) .property("contractTerms", codecForAny())
.property("proposalId", codecForString()) .property("proposalId", codecForString())
.property("noncePriv", codecForString())
.property( .property(
"status", "status",
codecForConstString(PreparePayResultType.InsufficientBalance), codecForConstString(PreparePayResultType.InsufficientBalance),
@ -354,6 +356,7 @@ export const codecForPreparePayResultAlreadyConfirmed = (): Codec<PreparePayResu
.property("contractTerms", codecForAny()) .property("contractTerms", codecForAny())
.property("contractTermsHash", codecForString()) .property("contractTermsHash", codecForString())
.property("proposalId", codecForString()) .property("proposalId", codecForString())
.property("noncePriv", codecForString())
.build("PreparePayResultAlreadyConfirmed"); .build("PreparePayResultAlreadyConfirmed");
export const codecForPreparePayResult = (): Codec<PreparePayResult> => export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
@ -385,6 +388,7 @@ export interface PreparePayResultPaymentPossible {
contractTermsHash: string; contractTermsHash: string;
amountRaw: string; amountRaw: string;
amountEffective: string; amountEffective: string;
noncePriv: string;
} }
export interface PreparePayResultInsufficientBalance { export interface PreparePayResultInsufficientBalance {
@ -392,6 +396,7 @@ export interface PreparePayResultInsufficientBalance {
proposalId: string; proposalId: string;
contractTerms: ContractTerms; contractTerms: ContractTerms;
amountRaw: string; amountRaw: string;
noncePriv: string;
} }
export interface PreparePayResultAlreadyConfirmed { export interface PreparePayResultAlreadyConfirmed {
@ -402,6 +407,7 @@ export interface PreparePayResultAlreadyConfirmed {
amountEffective: string; amountEffective: string;
contractTermsHash: string; contractTermsHash: string;
proposalId: string; proposalId: string;
noncePriv: string;
} }
export interface BankWithdrawDetails { export interface BankWithdrawDetails {

View File

@ -213,7 +213,7 @@ export class CryptoApi {
ws.w = null; ws.w = null;
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e as string);
} }
if (ws.currentWorkItem !== null) { if (ws.currentWorkItem !== null) {
ws.currentWorkItem.reject(e); ws.currentWorkItem.reject(e);
@ -379,6 +379,10 @@ export class CryptoApi {
return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1);
} }
eddsaGetPublic(key: string): Promise<{ priv: string; pub: string }> {
return this.doRpc<{ priv: string; pub: string }>("eddsaGetPublic", 1, key);
}
rsaUnblind(sig: string, bk: string, pk: string): Promise<string> { rsaUnblind(sig: string, bk: string, pk: string): Promise<string> {
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk); return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
} }

View File

@ -62,6 +62,7 @@ import {
setupRefreshTransferPub, setupRefreshTransferPub,
setupTipPlanchet, setupTipPlanchet,
setupWithdrawPlanchet, setupWithdrawPlanchet,
eddsaGetPublic,
} from "../talerCrypto.js"; } from "../talerCrypto.js";
import { randomBytes } from "../primitives/nacl-fast.js"; import { randomBytes } from "../primitives/nacl-fast.js";
import { kdf } from "../primitives/kdf.js"; import { kdf } from "../primitives/kdf.js";
@ -141,7 +142,7 @@ function timestampRoundedToBuffer(ts: Timestamp): Uint8Array {
class SignaturePurposeBuilder { class SignaturePurposeBuilder {
private chunks: Uint8Array[] = []; private chunks: Uint8Array[] = [];
constructor(private purposeNum: number) {} constructor(private purposeNum: number) { }
put(bytes: Uint8Array): SignaturePurposeBuilder { put(bytes: Uint8Array): SignaturePurposeBuilder {
this.chunks.push(Uint8Array.from(bytes)); this.chunks.push(Uint8Array.from(bytes));
@ -170,7 +171,6 @@ class SignaturePurposeBuilder {
function buildSigPS(purposeNum: number): SignaturePurposeBuilder { function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
return new SignaturePurposeBuilder(purposeNum); return new SignaturePurposeBuilder(purposeNum);
} }
export class CryptoImplementation { export class CryptoImplementation {
static enableTracing = false; static enableTracing = false;
@ -361,6 +361,13 @@ export class CryptoImplementation {
}; };
} }
eddsaGetPublic(key: string): { priv: string; pub: string } {
return {
priv: key,
pub: encodeCrock(eddsaGetPublic(decodeCrock(key)))
}
}
/** /**
* Unblind a blindly signed value. * Unblind a blindly signed value.
*/ */

View File

@ -875,7 +875,9 @@ async function startDownloadProposal(
orderId: string, orderId: string,
sessionId: string | undefined, sessionId: string | undefined,
claimToken: string | undefined, claimToken: string | undefined,
noncePriv: string | undefined,
): Promise<string> { ): Promise<string> {
const oldProposal = await ws.db const oldProposal = await ws.db
.mktx((x) => ({ proposals: x.proposals })) .mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
@ -884,12 +886,20 @@ async function startDownloadProposal(
orderId, orderId,
]); ]);
}); });
if (oldProposal) {
/**
* If we have already claimed this proposal with the same sessionId
* nonce and claim token, reuse it.
*/
if (oldProposal &&
oldProposal.downloadSessionId === sessionId &&
oldProposal.noncePriv === noncePriv &&
oldProposal.claimToken === claimToken) {
await processDownloadProposal(ws, oldProposal.proposalId); await processDownloadProposal(ws, oldProposal.proposalId);
return oldProposal.proposalId; return oldProposal.proposalId;
} }
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair(); const { priv, pub } = await (noncePriv ? ws.cryptoApi.eddsaGetPublic(noncePriv) : ws.cryptoApi.createEddsaKeypair());
const proposalId = encodeCrock(getRandomBytes(32)); const proposalId = encodeCrock(getRandomBytes(32));
const proposalRecord: ProposalRecord = { const proposalRecord: ProposalRecord = {
@ -1405,6 +1415,7 @@ export async function checkPaymentByProposalId(
status: PreparePayResultType.InsufficientBalance, status: PreparePayResultType.InsufficientBalance,
contractTerms: d.contractTermsRaw, contractTerms: d.contractTermsRaw,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
noncePriv: proposal.noncePriv,
amountRaw: Amounts.stringify(d.contractData.amount), amountRaw: Amounts.stringify(d.contractData.amount),
}; };
} }
@ -1417,6 +1428,7 @@ export async function checkPaymentByProposalId(
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
contractTerms: d.contractTermsRaw, contractTerms: d.contractTermsRaw,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
noncePriv: proposal.noncePriv,
amountEffective: Amounts.stringify(totalCost), amountEffective: Amounts.stringify(totalCost),
amountRaw: Amounts.stringify(res.paymentAmount), amountRaw: Amounts.stringify(res.paymentAmount),
contractTermsHash: d.contractData.contractTermsHash, contractTermsHash: d.contractData.contractTermsHash,
@ -1453,6 +1465,7 @@ export async function checkPaymentByProposalId(
amountRaw: Amounts.stringify(purchase.download.contractData.amount), amountRaw: Amounts.stringify(purchase.download.contractData.amount),
amountEffective: Amounts.stringify(purchase.totalPayCost), amountEffective: Amounts.stringify(purchase.totalPayCost),
proposalId, proposalId,
noncePriv: proposal.noncePriv,
}; };
} else if (!purchase.timestampFirstSuccessfulPay) { } else if (!purchase.timestampFirstSuccessfulPay) {
return { return {
@ -1463,6 +1476,7 @@ export async function checkPaymentByProposalId(
amountRaw: Amounts.stringify(purchase.download.contractData.amount), amountRaw: Amounts.stringify(purchase.download.contractData.amount),
amountEffective: Amounts.stringify(purchase.totalPayCost), amountEffective: Amounts.stringify(purchase.totalPayCost),
proposalId, proposalId,
noncePriv: proposal.noncePriv,
}; };
} else { } else {
const paid = !purchase.paymentSubmitPending; const paid = !purchase.paymentSubmitPending;
@ -1475,6 +1489,7 @@ export async function checkPaymentByProposalId(
amountEffective: Amounts.stringify(purchase.totalPayCost), amountEffective: Amounts.stringify(purchase.totalPayCost),
...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}), ...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}),
proposalId, proposalId,
noncePriv: proposal.noncePriv,
}; };
} }
} }
@ -1507,6 +1522,7 @@ export async function preparePayForUri(
uriResult.orderId, uriResult.orderId,
uriResult.sessionId, uriResult.sessionId,
uriResult.claimToken, uriResult.claimToken,
uriResult.noncePriv,
); );
return checkPaymentByProposalId(ws, proposalId, uriResult.sessionId); return checkPaymentByProposalId(ws, proposalId, uriResult.sessionId);

View File

@ -22,6 +22,7 @@
"history": "4.10.1", "history": "4.10.1",
"preact": "^10.5.13", "preact": "^10.5.13",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"qrcode-generator": "^1.4.4",
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,37 @@
/*
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/>
*/
import { h, VNode } from "preact";
import { useEffect, useRef } from "preact/hooks";
import qrcode from "qrcode-generator";
export function QR({ text }: { text: string; }):VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!divRef.current) return
const qr = qrcode(0, 'L');
qr.addData(text);
qr.make();
divRef.current.innerHTML = qr.createSvgTag({
scalable: true,
});
});
return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
</div>;
}

View File

@ -29,10 +29,11 @@ export const PaymentStatus = styled.div<{ color: string }>`
export const WalletAction = styled.section` export const WalletAction = styled.section`
display: flex; display: flex;
text-align: center;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
max-width: 50%; /* max-width: 50%; */
margin: auto; margin: auto;
height: 100%; height: 100%;
@ -42,6 +43,10 @@ export const WalletAction = styled.section`
} }
section { section {
margin-bottom: 2em; margin-bottom: 2em;
& button {
margin-right: 8px;
margin-left: 8px;
}
} }
` `
export const WalletActionOld = styled.section` export const WalletActionOld = styled.section`
@ -628,6 +633,7 @@ export const TermsOfService = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
max-width: 500px;
& > header { & > header {
text-align: center; text-align: center;

View File

@ -33,6 +33,7 @@ export default {
export const InsufficientBalance = createExample(TestedComponent, { export const InsufficientBalance = createExample(TestedComponent, {
payStatus: { payStatus: {
status: PreparePayResultType.InsufficientBalance, status: PreparePayResultType.InsufficientBalance,
noncePriv: '',
proposalId: "proposal1234", proposalId: "proposal1234",
contractTerms: { contractTerms: {
merchant: { merchant: {
@ -45,15 +46,19 @@ export const InsufficientBalance = createExample(TestedComponent, {
}); });
export const PaymentPossible = createExample(TestedComponent, { export const PaymentPossible = createExample(TestedComponent, {
uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
payStatus: { payStatus: {
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
amountEffective: 'USD:10', amountEffective: 'USD:10',
amountRaw: 'USD:10', amountRaw: 'USD:10',
noncePriv: '',
contractTerms: { contractTerms: {
nonce: '123213123',
merchant: { merchant: {
name: 'someone' name: 'someone'
}, },
amount: 'USD:10', amount: 'USD:10',
summary: 'some beers',
} as Partial<ContractTerms> as any, } as Partial<ContractTerms> as any,
contractTermsHash: '123456', contractTermsHash: '123456',
proposalId: 'proposal1234' proposalId: 'proposal1234'
@ -65,6 +70,7 @@ export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
amountEffective: 'USD:10', amountEffective: 'USD:10',
amountRaw: 'USD:10', amountRaw: 'USD:10',
noncePriv: '',
contractTerms: { contractTerms: {
merchant: { merchant: {
name: 'someone' name: 'someone'
@ -82,6 +88,7 @@ export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent,
payStatus: { payStatus: {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
amountEffective: 'USD:10', amountEffective: 'USD:10',
noncePriv: '',
amountRaw: 'USD:10', amountRaw: 'USD:10',
contractTerms: { contractTerms: {
merchant: { merchant: {

View File

@ -29,7 +29,7 @@ import * as wxApi from "../wxApi";
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { ConfirmPayResultDone, getJsonI18n, i18n } from "@gnu-taler/taler-util"; import { AmountLike, ConfirmPayResultDone, getJsonI18n, i18n } from "@gnu-taler/taler-util";
import { import {
PreparePayResult, PreparePayResult,
ConfirmPayResult, ConfirmPayResult,
@ -39,7 +39,11 @@ import {
ContractTerms, ContractTerms,
ConfirmPayResultType, ConfirmPayResultType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { JSX, VNode, h } from "preact"; import { JSX, VNode, h, Fragment } from "preact";
import { ButtonSuccess, LinkSuccess, WalletAction } from "../components/styled";
import { LogoHeader } from "../components/LogoHeader";
import { Part } from "../components/Part";
import { QR } from "../components/QR";
interface Props { interface Props {
talerPayUri?: string talerPayUri?: string
@ -143,17 +147,17 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
} }
return <PaymentRequestView payStatus={payStatus} onClick={onClick} payErrMsg={payErrMsg} />; return <PaymentRequestView uri={talerPayUri} payStatus={payStatus} onClick={onClick} payErrMsg={payErrMsg} />;
} }
export interface PaymentRequestViewProps { export interface PaymentRequestViewProps {
payStatus: PreparePayResult; payStatus: PreparePayResult;
onClick: () => void; onClick: () => void;
payErrMsg?: string; payErrMsg?: string;
uri: string;
} }
export function PaymentRequestView({ payStatus, onClick, payErrMsg }: PaymentRequestViewProps) { export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg }: PaymentRequestViewProps) {
let totalFees: AmountJson | undefined = undefined; let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
let insufficientBalance = false; let insufficientBalance = false;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const contractTerms: ContractTerms = payStatus.contractTerms; const contractTerms: ContractTerms = payStatus.contractTerms;
@ -174,6 +178,7 @@ export function PaymentRequestView({ payStatus, onClick, payErrMsg }: PaymentReq
if (payStatus.status == PreparePayResultType.InsufficientBalance) { if (payStatus.status == PreparePayResultType.InsufficientBalance) {
insufficientBalance = true; insufficientBalance = true;
return <div>no te alcanza</div>
} }
if (payStatus.status === PreparePayResultType.PaymentPossible) { if (payStatus.status === PreparePayResultType.PaymentPossible) {
@ -191,65 +196,62 @@ export function PaymentRequestView({ payStatus, onClick, payErrMsg }: PaymentReq
merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>; merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
} }
const amount = ( const [showQR, setShowQR] = useState<boolean>(false)
<strong>{renderAmount(Amounts.parseOrThrow(contractTerms.amount))}</strong> const privateUri = `${uri}&n=${payStatus.noncePriv}`
); return <WalletAction>
<LogoHeader />
<h2>
{i18n.str`Digital cash payment`}
</h2>
<section>
<Part big title="Total paid" text={amountToString(payStatus.amountEffective)} kind='negative' />
<Part big title="Purchase amount" text={amountToString(payStatus.amountRaw)} kind='neutral' />
{Amounts.isNonZero(totalFees) && <Part big title="Fee" text={amountToString(totalFees)} kind='negative' />}
<Part title="Merchant" text={contractTerms.merchant.name} kind='neutral' />
<Part title="Purchase" text={contractTerms.summary} kind='neutral' />
{contractTerms.order_id && <Part title="Receipt" text={`#${contractTerms.order_id}`} kind='neutral' />}
</section>
{showQR && <section>
<QR text={privateUri} />
<a href={privateUri}>or click here to pay with a installed wallet</a>
</section>}
<section>
{payErrMsg ? (
<div>
<p>Payment failed: {payErrMsg}</p>
<button
class="pure-button button-success"
onClick={onClick}
>
{i18n.str`Retry`}
</button>
</div>
) : (
<Fragment>
return <section class="main"> <LinkSuccess
<h1>GNU Taler Wallet</h1> upperCased
<article class="fade"> // disabled={!details.exchangeInfo.baseUrl}
<div> onClick={() => setShowQR(qr => !qr)}
<p> >
<i18n.Translate> {!showQR ? i18n.str`Complete with mobile wallet` : i18n.str`Hide QR`}
The merchant <span>{merchantName}</span> offers you to purchase: </LinkSuccess>
</i18n.Translate> <ButtonSuccess
<div style={{ textAlign: "center" }}> upperCased
<strong>{contractTerms.summary}</strong> // disabled={!details.exchangeInfo.baseUrl}
</div> // onClick={() => onReview(true)}
{totalFees ? ( >
<i18n.Translate> {i18n.str`Confirm payment`}
The total price is <span>{amount} </span> </ButtonSuccess>
(plus <span>{renderAmount(totalFees)}</span> fees). </Fragment>
</i18n.Translate> )}
) : (
<i18n.Translate>
The total price is <span>{amount}</span>.
</i18n.Translate>
)}
</p>
{insufficientBalance ? ( </section>
<div> </WalletAction>
<p style={{ color: "red", fontWeight: "bold" }}> }
Unable to pay: Your balance is insufficient.
</p>
</div>
) : null}
{payErrMsg ? ( function amountToString(text: AmountLike) {
<div> const aj = Amounts.jsonifyAmount(text)
<p>Payment failed: {payErrMsg}</p> const amount = Amounts.stringifyValue(aj)
<button return `${amount} ${aj.currency}`
class="pure-button button-success" }
onClick={onClick}
>
{i18n.str`Retry`}
</button>
</div>
) : (
<div>
<ProgressButton
isLoading={loading}
disabled={insufficientBalance}
onClick={onClick}
>
{i18n.str`Confirm payment`}
</ProgressButton>
</div>
)}
</div>
</article>
</section>
}

View File

@ -80,20 +80,18 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview,
const needsReview = terms.status === 'changed' || terms.status === 'new' const needsReview = terms.status === 'changed' || terms.status === 'new'
return ( return (
<WalletAction style={{ textAlign: 'center' }}> <WalletAction>
<LogoHeader /> <LogoHeader />
<h2> <h2>
{i18n.str`Digital cash withdrawal`} {i18n.str`Digital cash withdrawal`}
</h2> </h2>
<section> <section>
<div> <Part title="Total to withdraw" text={amountToString(Amounts.sub(Amounts.parseOrThrow(amount), details.withdrawFee).amount)} kind='positive' />
<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' />
<Part title="Chosen amount" text={amountToString(amount)} kind='neutral' /> {Amounts.isNonZero(details.withdrawFee) &&
{Amounts.isNonZero(details.withdrawFee) && <Part title="Exchange fee" text={amountToString(details.withdrawFee)} kind='negative' />
<Part title="Exchange fee" text={amountToString(details.withdrawFee)} kind='negative' /> }
} <Part title="Exchange" text={details.exchangeInfo.baseUrl} kind='neutral' big />
<Part title="Exchange" text={details.exchangeInfo.baseUrl} kind='neutral' big />
</div>
</section> </section>
{!reviewing && {!reviewing &&
<section> <section>
@ -132,63 +130,50 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview,
} }
{(reviewing || accepted) && {(reviewing || accepted) &&
<section> <section>
<div> <CheckboxOutlined
<CheckboxOutlined name="terms"
name="terms" enabled={accepted}
enabled={accepted} label={i18n.str`I accept the exchange terms of service`}
label={i18n.str`I accept the exchange terms of service`} onToggle={() => {
onToggle={() => { onAccept(!accepted)
onAccept(!accepted) onReview(false)
onReview(false) }}
}} />
/>
</div>
</section> </section>
} }
<section> <section>
{terms.status === 'new' && !accepted && {terms.status === 'new' && !accepted &&
<div> <ButtonSuccess
<ButtonSuccess upperCased
upperCased disabled={!details.exchangeInfo.baseUrl}
disabled={!details.exchangeInfo.baseUrl} onClick={() => onReview(true)}
onClick={() => onReview(true)} >
> {i18n.str`Review exchange terms of service`}
{i18n.str`Review exchange terms of service`} </ButtonSuccess>
</ButtonSuccess>
</div>
} }
{terms.status === 'changed' && !accepted && {terms.status === 'changed' && !accepted &&
<div> <ButtonWarning
<ButtonWarning upperCased
upperCased disabled={!details.exchangeInfo.baseUrl}
disabled={!details.exchangeInfo.baseUrl} onClick={() => onReview(true)}
onClick={() => onReview(true)} >
> {i18n.str`Review new version of terms of service`}
{i18n.str`Review new version of terms of service`} </ButtonWarning>
</ButtonWarning>
</div>
} }
{(terms.status === 'accepted' || (needsReview && accepted)) && {(terms.status === 'accepted' || (needsReview && accepted)) &&
<div> <ButtonSuccess
<ButtonSuccess upperCased
upperCased disabled={!details.exchangeInfo.baseUrl || confirmed}
disabled={!details.exchangeInfo.baseUrl || confirmed} onClick={onWithdraw}
onClick={onWithdraw} >
> {i18n.str`Confirm withdrawal`}
{i18n.str`Confirm withdrawal`} </ButtonSuccess>
</ButtonSuccess>
</div>
} }
{terms.status === 'notfound' && {terms.status === 'notfound' &&
<div> <ButtonDestructive upperCased disabled>
<ButtonDestructive {i18n.str`Exchange doesn't have terms of service`}
upperCased </ButtonDestructive>
disabled={true}
>
{i18n.str`Exchange doesn't have terms of service`}
</ButtonDestructive>
</div>
} }
</section> </section>
</WalletAction> </WalletAction>
@ -231,12 +216,16 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
if (!uriInfo || !uriInfo.defaultExchangeBaseUrl) return if (!uriInfo || !uriInfo.defaultExchangeBaseUrl) return
const res = await getExchangeWithdrawalInfo({ try {
exchangeBaseUrl: uriInfo.defaultExchangeBaseUrl, const res = await getExchangeWithdrawalInfo({
amount: Amounts.parseOrThrow(uriInfo.amount), exchangeBaseUrl: uriInfo.defaultExchangeBaseUrl,
tosAcceptedFormat: ['text/json', 'text/xml', 'text/pdf'] amount: Amounts.parseOrThrow(uriInfo.amount),
}) tosAcceptedFormat: ['text/json', 'text/xml', 'text/pdf']
setDetails(res) })
setDetails(res)
} catch (e) {
setError(true)
}
} }
fetchData() fetchData()
}, [uriInfo]) }, [uriInfo])
@ -249,8 +238,12 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
if (!details) { if (!details) {
throw Error("can't accept, no exchange selected"); throw Error("can't accept, no exchange selected");
} }
await setExchangeTosAccepted(details.exchangeDetails.exchangeBaseUrl, details.tosRequested?.tosEtag) try {
setAccepted(true) await setExchangeTosAccepted(details.exchangeDetails.exchangeBaseUrl, details.tosRequested?.tosEtag)
setAccepted(true)
} catch (e) {
setError(true)
}
} }
const onWithdraw = async (): Promise<void> => { const onWithdraw = async (): Promise<void> => {
@ -259,10 +252,14 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
} }
setConfirmed(true) setConfirmed(true)
console.log("accepting exchange", details.exchangeInfo.baseUrl); console.log("accepting exchange", details.exchangeInfo.baseUrl);
const res = await acceptWithdrawal(talerWithdrawUri, details.exchangeInfo.baseUrl); try {
console.log("accept withdrawal response", res); const res = await acceptWithdrawal(talerWithdrawUri, details.exchangeInfo.baseUrl);
if (res.confirmTransferUrl) { console.log("accept withdrawal response", res);
document.location.href = res.confirmTransferUrl; if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
}
} catch (e) {
setConfirmed(false)
} }
}; };
@ -288,7 +285,7 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element
} catch (e) { } catch (e) {
console.log(e) console.log(e)
debugger; debugger;
} }
} }
} }

View File

@ -248,6 +248,7 @@ importers:
preact-cli: ^3.0.5 preact-cli: ^3.0.5
preact-render-to-string: ^5.1.19 preact-render-to-string: ^5.1.19
preact-router: ^3.2.1 preact-router: ^3.2.1
qrcode-generator: ^1.4.4
rimraf: ^3.0.2 rimraf: ^3.0.2
rollup: ^2.37.1 rollup: ^2.37.1
rollup-plugin-css-only: ^3.1.0 rollup-plugin-css-only: ^3.1.0
@ -264,6 +265,7 @@ importers:
history: 4.10.1 history: 4.10.1
preact: 10.5.14 preact: 10.5.14
preact-router: 3.2.1_preact@10.5.14 preact-router: 3.2.1_preact@10.5.14
qrcode-generator: 1.4.4
tslib: 2.3.1 tslib: 2.3.1
devDependencies: devDependencies:
'@babel/core': 7.13.16 '@babel/core': 7.13.16
@ -16698,6 +16700,10 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'} engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true dev: true
/qrcode-generator/1.4.4:
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
dev: false
/qs/6.10.1: /qs/6.10.1:
resolution: {integrity: sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==} resolution: {integrity: sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}