/* This file is part of TALER (C) 2015 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 */ /** * Page shown to the user to confirm entering * a contract. */ /** * Imports. */ import * as i18n from "../../i18n"; import { ExchangeRecord, ProposalDownloadRecord, } from "../../dbTypes"; import { ContractTerms } from "../../talerTypes"; import { CheckPayResult, } from "../../walletTypes"; import { renderAmount } from "../renderHtml"; import * as wxApi from "../wxApi"; import * as React from "react"; import * as ReactDOM from "react-dom"; import URI = require("urijs"); import { WalletApiError } from "../wxApi"; interface DetailState { collapsed: boolean; } interface DetailProps { contractTerms: ContractTerms; collapsed: boolean; exchanges: ExchangeRecord[] | undefined; } class Details extends React.Component { constructor(props: DetailProps) { super(props); console.log("new Details component created"); this.state = { collapsed: props.collapsed, }; console.log("initial state:", this.state); } render() { if (this.state.collapsed) { return (
); } else { return (
{i18n.str`Accepted exchanges:`}
    {this.props.contractTerms.exchanges.map( (e) =>
  • {`${e.url}: ${e.master_pub}`}
  • )}
{i18n.str`Exchanges in the wallet:`}
    {(this.props.exchanges || []).map( (e: ExchangeRecord) =>
  • {`${e.baseUrl}: ${e.masterPublicKey}`}
  • )}
); } } } interface ContractPromptProps { proposalId?: number; contractUrl?: string; sessionId?: string; resourceUrl?: string; } interface ContractPromptState { proposalId: number | undefined; proposal: ProposalDownloadRecord | undefined; checkPayError: string | undefined; confirmPayError: object | undefined; payDisabled: boolean; alreadyPaid: boolean; exchanges: ExchangeRecord[] | undefined; /** * Don't request updates to proposal state while * this is set to true, to avoid UI flickering * when pressing pay. */ holdCheck: boolean; payStatus?: CheckPayResult; replaying: boolean; payInProgress: boolean; payAttempt: number; working: boolean; abortDone: boolean; abortStarted: boolean; } class ContractPrompt extends React.Component { constructor(props: ContractPromptProps) { super(props); this.state = { abortDone: false, abortStarted: false, alreadyPaid: false, checkPayError: undefined, confirmPayError: undefined, exchanges: undefined, holdCheck: false, payAttempt: 0, payDisabled: true, payInProgress: false, proposal: undefined, proposalId: props.proposalId, replaying: false, working: false, }; } componentWillMount() { this.update(); } componentWillUnmount() { // FIXME: abort running ops } async update() { if (this.props.resourceUrl) { const p = await wxApi.queryPaymentByFulfillmentUrl(this.props.resourceUrl); console.log("query for resource url", this.props.resourceUrl, "result", p); if (p && p.finished) { if (p.lastSessionSig === undefined || p.lastSessionSig === this.props.sessionId) { const nextUrl = new URI(p.contractTerms.fulfillment_url); nextUrl.addSearch("order_id", p.contractTerms.order_id); if (p.lastSessionSig) { nextUrl.addSearch("session_sig", p.lastSessionSig); } location.replace(nextUrl.href()); return; } else { // We're in a new session this.setState({ replaying: true }); // FIXME: This could also go wrong. However the payment // was already successful once, so we can just retry and not refund it. const payResult = await wxApi.submitPay(p.contractTermsHash, this.props.sessionId); console.log("payResult", payResult); location.replace(payResult.nextUrl); return; } } } let proposalId = this.props.proposalId; if (proposalId === undefined) { if (this.props.contractUrl === undefined) { // Nothing we can do ... return; } proposalId = await wxApi.downloadProposal(this.props.contractUrl); } const proposal = await wxApi.getProposal(proposalId); this.setState({ proposal, proposalId }); this.checkPayment(); const exchanges = await wxApi.getExchanges(); this.setState({ exchanges }); } async checkPayment() { window.setTimeout(() => this.checkPayment(), 500); if (this.state.holdCheck) { return; } const proposalId = this.state.proposalId; if (proposalId === undefined) { return; } const payStatus = await wxApi.checkPay(proposalId); if (payStatus.status === "insufficient-balance") { const msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`; // tslint:disable-next-line:max-line-length const msgNoMatch = i18n.str`You do not have any funds from an exchange that is accepted by this merchant. None of the exchanges accepted by the merchant is known to your wallet.`; if (this.state.exchanges && this.state.proposal) { const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub); const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0); if (ex) { this.setState({ checkPayError: msgInsufficient }); } else { this.setState({ checkPayError: msgNoMatch }); } } else { this.setState({ checkPayError: msgInsufficient }); } this.setState({ payDisabled: true }); } else if (payStatus.status === "paid") { this.setState({ alreadyPaid: true, payDisabled: false, checkPayError: undefined, payStatus }); } else { this.setState({ payDisabled: false, checkPayError: undefined, payStatus }); } } async doPayment() { const proposal = this.state.proposal; this.setState({ holdCheck: true, payAttempt: this.state.payAttempt + 1}); if (!proposal) { return; } const proposalId = proposal.id; if (proposalId === undefined) { console.error("proposal has no id"); return; } console.log("confirmPay with", proposalId, "and", this.props.sessionId); let payResult; this.setState({ working: true }); try { payResult = await wxApi.confirmPay(proposalId, this.props.sessionId); } catch (e) { if (!(e instanceof WalletApiError)) { throw e; } this.setState({ confirmPayError: e.detail }); return; } finally { } console.log("payResult", payResult); document.location.href = payResult.nextUrl; this.setState({ holdCheck: true }); } async abortPayment() { const proposal = this.state.proposal; this.setState({ holdCheck: true, abortStarted: true }); if (!proposal) { return; } wxApi.abortFailedPayment(proposal.contractTermsHash); this.setState({ abortDone: true }); } render() { if (this.props.contractUrl === undefined && this.props.proposalId === undefined) { return Error: either contractUrl or proposalId must be given; } if (this.state.replaying) { return Re-submitting existing payment; } if (this.state.proposalId === undefined) { return Downloading contract terms; } if (!this.state.proposal) { return ...; } const c = this.state.proposal.contractTerms; let merchantName; if (c.merchant && c.merchant.name) { merchantName = {c.merchant.name}; } else { merchantName = (pub: {c.merchant_pub}); } const amount = {renderAmount(c.amount)}; console.log("payStatus", this.state.payStatus); let products = null; if (c.products.length) { products = (
The following items are included:
    {c.products.map( (p: any, i: number) => (
  • {p.description}: {renderAmount(p.price)}
  • )) }
); } const ConfirmButton = () => ( ); const WorkingButton = () => (