From defbf625bdef0f8a666b72b8ce99de5e01af6b91 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Aug 2019 23:12:55 +0200 Subject: url-based pay/withdraw, use react hooks --- src/webex/messages.ts | 68 ++-- src/webex/pages/confirm-contract.html | 79 ----- src/webex/pages/confirm-contract.tsx | 417 ---------------------- src/webex/pages/confirm-create-reserve.html | 24 -- src/webex/pages/confirm-create-reserve.tsx | 526 ---------------------------- src/webex/pages/pay.html | 79 +++++ src/webex/pages/pay.tsx | 173 +++++++++ src/webex/pages/withdraw.html | 24 ++ src/webex/pages/withdraw.tsx | 231 ++++++++++++ src/webex/style/wallet.css | 5 + src/webex/wxApi.ts | 23 ++ src/webex/wxBackend.ts | 262 ++++---------- 12 files changed, 644 insertions(+), 1267 deletions(-) delete mode 100644 src/webex/pages/confirm-contract.html delete mode 100644 src/webex/pages/confirm-contract.tsx delete mode 100644 src/webex/pages/confirm-create-reserve.html delete mode 100644 src/webex/pages/confirm-create-reserve.tsx create mode 100644 src/webex/pages/pay.html create mode 100644 src/webex/pages/pay.tsx create mode 100644 src/webex/pages/withdraw.html create mode 100644 src/webex/pages/withdraw.tsx (limited to 'src/webex') diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 8bb9cafe5..ca0e1c7e1 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -32,12 +32,12 @@ import { UpgradeResponse } from "./wxApi"; * Message type information. */ export interface MessageMap { - "balances": { - request: { }; + balances: { + request: {}; response: walletTypes.WalletBalance; }; "dump-db": { - request: { }; + request: {}; response: any; }; "import-db": { @@ -46,18 +46,18 @@ export interface MessageMap { }; response: void; }; - "ping": { - request: { }; + ping: { + request: {}; response: void; }; "reset-db": { - request: { }; + request: {}; response: void; }; "create-reserve": { request: { amount: AmountJson; - exchange: string + exchange: string; }; response: void; }; @@ -70,11 +70,11 @@ export interface MessageMap { response: walletTypes.ConfirmPayResult; }; "check-pay": { - request: { proposalId: number; }; + request: { proposalId: number }; response: walletTypes.CheckPayResult; }; "query-payment": { - request: { }; + request: {}; response: dbTypes.PurchaseRecord; }; "exchange-info": { @@ -90,11 +90,11 @@ export interface MessageMap { response: string; }; "reserve-creation-info": { - request: { baseUrl: string, amount: AmountJson }; + request: { baseUrl: string; amount: AmountJson }; response: walletTypes.ReserveCreationInfo; }; "get-history": { - request: { }; + request: {}; response: walletTypes.HistoryRecord[]; }; "get-proposal": { @@ -110,7 +110,7 @@ export interface MessageMap { response: any; }; "get-currencies": { - request: { }; + request: {}; response: dbTypes.CurrencyRecord[]; }; "update-currency": { @@ -118,7 +118,7 @@ export interface MessageMap { response: void; }; "get-exchanges": { - request: { }; + request: {}; response: dbTypes.ExchangeRecord[]; }; "get-reserves": { @@ -126,7 +126,7 @@ export interface MessageMap { response: dbTypes.ReserveRecord[]; }; "get-payback-reserves": { - request: { }; + request: {}; response: dbTypes.ReserveRecord[]; }; "withdraw-payback-reserve": { @@ -146,15 +146,15 @@ export interface MessageMap { response: void; }; "check-upgrade": { - request: { }; + request: {}; response: UpgradeResponse; }; "get-sender-wire-infos": { - request: { }; + request: {}; response: walletTypes.SenderWireInfos; }; "return-coins": { - request: { }; + request: {}; response: void; }; "log-and-display-error": { @@ -182,7 +182,7 @@ export interface MessageMap { response: walletTypes.TipStatus; }; "clear-notification": { - request: { }; + request: {}; response: void; }; "taler-pay": { @@ -194,23 +194,36 @@ export interface MessageMap { response: number; }; "submit-pay": { - request: { contractTermsHash: string, sessionId: string | undefined }; + request: { contractTermsHash: string; sessionId: string | undefined }; response: walletTypes.ConfirmPayResult; }; "accept-refund": { - request: { refundUrl: string } + request: { refundUrl: string }; response: string; }; "abort-failed-payment": { - request: { contractTermsHash: string } + request: { contractTermsHash: string }; response: void; }; "benchmark-crypto": { - request: { repetitions: number } + request: { repetitions: number }; response: walletTypes.BenchmarkResult; }; + "get-withdraw-details": { + request: { talerWithdrawUri: string; maybeSelectedExchange: string | undefined }; + response: walletTypes.WithdrawDetails; + }; + "accept-withdrawal": { + request: { talerWithdrawUri: string; selectedExchange: string }; + response: walletTypes.AcceptWithdrawalResponse; + }; + "prepare-pay": { + request: { talerPayUri: string }; + response: walletTypes.PreparePayResult; + }; } + /** * String literal types for messages. */ @@ -219,14 +232,19 @@ export type MessageType = keyof MessageMap; /** * Make a request whose details match the request type. */ -export function makeRequest(type: T, details: MessageMap[T]["request"]) { +export function makeRequest( + type: T, + details: MessageMap[T]["request"], +) { return { type, details }; } /** * Make a response that matches the request type. */ -export function makeResponse(type: T, response: MessageMap[T]["response"]) { +export function makeResponse( + type: T, + response: MessageMap[T]["response"], +) { return response; } - diff --git a/src/webex/pages/confirm-contract.html b/src/webex/pages/confirm-contract.html deleted file mode 100644 index 5a949159a..000000000 --- a/src/webex/pages/confirm-contract.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Taler Wallet: Confirm Contract - - - - - - - - - - - - - -
-

GNU Taler Wallet

-
-
- - - diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx deleted file mode 100644 index d24613794..000000000 --- a/src/webex/pages/confirm-contract.tsx +++ /dev/null @@ -1,417 +0,0 @@ -/* - 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 { runOnceWhenReady } from "./common"; - -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"; - -import * as Amounts from "../../amounts"; - - -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; - } - console.log("payResult", payResult); - document.location.replace(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(Amounts.parseOrThrow(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 = () => ( -
-