diff options
Diffstat (limited to 'src/webex')
| -rw-r--r-- | src/webex/messages.ts | 68 | ||||
| -rw-r--r-- | src/webex/pages/confirm-contract.tsx | 417 | ||||
| -rw-r--r-- | src/webex/pages/confirm-create-reserve.tsx | 526 | ||||
| -rw-r--r-- | src/webex/pages/pay.html (renamed from src/webex/pages/confirm-contract.html) | 2 | ||||
| -rw-r--r-- | src/webex/pages/pay.tsx | 173 | ||||
| -rw-r--r-- | src/webex/pages/withdraw.html (renamed from src/webex/pages/confirm-create-reserve.html) | 2 | ||||
| -rw-r--r-- | src/webex/pages/withdraw.tsx | 231 | ||||
| -rw-r--r-- | src/webex/style/wallet.css | 5 | ||||
| -rw-r--r-- | src/webex/wxApi.ts | 23 | ||||
| -rw-r--r-- | src/webex/wxBackend.ts | 262 | 
10 files changed, 543 insertions, 1166 deletions
| 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<T extends MessageType>(type: T, details: MessageMap[T]["request"]) { +export function makeRequest<T extends MessageType>( +  type: T, +  details: MessageMap[T]["request"], +) {    return { type, details };  }  /**   * Make a response that matches the request type.   */ -export function makeResponse<T extends MessageType>(type: T, response: MessageMap[T]["response"]) { +export function makeResponse<T extends MessageType>( +  type: T, +  response: MessageMap[T]["response"], +) {    return response;  } - 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 <http://www.gnu.org/licenses/> - */ - -/** - * 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<DetailProps, DetailState> { -  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 ( -        <div> -          <button className="linky" -                  onClick={() => { this.setState({collapsed: false} as any); }}> -          <i18n.Translate wrap="span"> -            show more details -          </i18n.Translate> -          </button> -        </div> -      ); -    } else { -      return ( -        <div> -          <button className="linky" -                  onClick={() => this.setState({collapsed: true} as any)}> -            i18n.str`show fewer details` -          </button> -          <div> -            {i18n.str`Accepted exchanges:`} -            <ul> -              {this.props.contractTerms.exchanges.map( -                (e) => <li>{`${e.url}: ${e.master_pub}`}</li>)} -            </ul> -            {i18n.str`Exchanges in the wallet:`} -            <ul> -              {(this.props.exchanges || []).map( -                (e: ExchangeRecord) => -                  <li>{`${e.baseUrl}: ${e.masterPublicKey}`}</li>)} -            </ul> -          </div> -        </div>); -    } -  } -} - -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<ContractPromptProps, ContractPromptState> { -  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 <span>Error: either contractUrl or proposalId must be given</span>; -    } -    if (this.state.replaying) { -      return <span>Re-submitting existing payment</span>; -    } -    if (this.state.proposalId === undefined) { -      return <span>Downloading contract terms</span>; -    } -    if (!this.state.proposal) { -      return <span>...</span>; -    } -    const c = this.state.proposal.contractTerms; -    let merchantName; -    if (c.merchant && c.merchant.name) { -      merchantName = <strong>{c.merchant.name}</strong>; -    } else { -      merchantName = <strong>(pub: {c.merchant_pub})</strong>; -    } -    const amount = <strong>{renderAmount(Amounts.parseOrThrow(c.amount))}</strong>; -    console.log("payStatus", this.state.payStatus); - -    let products = null; -    if (c.products.length) { -      products = ( -        <div> -          <span>The following items are included:</span> -          <ul> -            {c.products.map( -              (p: any, i: number) => (<li key={i}>{p.description}: {renderAmount(p.price)}</li>)) -            } -          </ul> -      </div> -      ); -    } - -    const ConfirmButton = () => ( -      <button className="pure-button button-success" -              disabled={this.state.payDisabled} -              onClick={() => this.doPayment()}> -        {i18n.str`Confirm payment`} -      </button> -    ); - -    const WorkingButton = () => ( -      <div> -      <button className="pure-button button-success" -              disabled={this.state.payDisabled} -              onClick={() => this.doPayment()}> -        <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span> -        {i18n.str`Submitting payment`} -      </button> -      </div> -    ); - -    const ConfirmPayDialog = () => ( -      <div> -        {this.state.working ? WorkingButton() : ConfirmButton()} -        <div> -          {(this.state.alreadyPaid -            ? <p className="okaybox"> -              {i18n.str`You already paid for this, clicking "Confirm payment" will not cost money again.`} -              </p> -            : <p />)} -          {(this.state.checkPayError ? <p className="errorbox">{this.state.checkPayError}</p> : <p />)} -        </div> -        <Details exchanges={this.state.exchanges} contractTerms={c} collapsed={!this.state.checkPayError}/> -      </div> -    ); - -    const PayErrorDialog = () => ( -      <div> -        <p>There was an error paying (attempt #{this.state.payAttempt}):</p> -        <pre>{JSON.stringify(this.state.confirmPayError)}</pre> -        { this.state.abortStarted -        ? <span>{i18n.str`Aborting payment ...`}</span> -        : this.state.abortDone -        ? <span>{i18n.str`Payment aborted!`}</span> -        : <> -            <button className="pure-button" onClick={() => this.doPayment()}> -            {i18n.str`Retry Payment`} -            </button> -            <button className="pure-button" onClick={() => this.abortPayment()}> -            {i18n.str`Abort Payment`} -            </button> -          </> -        } -      </div> -    ); - -    return ( -        <div> -          <i18n.Translate wrap="p"> -            The merchant{" "}<span>{merchantName}</span> offers you to purchase: -          </i18n.Translate> -          <div style={{"textAlign": "center"}}> -            <strong>{c.summary}</strong> -          </div> -          <strong></strong> -          {products} -          {(this.state.payStatus && this.state.payStatus.coinSelection) -            ? <i18n.Translate wrap="p"> -                The total price is <span>{amount} </span> -                (plus <span>{renderAmount(this.state.payStatus.coinSelection.totalFees)}</span> fees). -              </i18n.Translate> -            : -            <i18n.Translate wrap="p">The total price is <span>{amount}</span>.</i18n.Translate> -          } -          { this.state.confirmPayError -            ? PayErrorDialog() -            : ConfirmPayDialog() -          } -        </div> -    ); -  } -} - - -runOnceWhenReady(() => { -  const url = new URI(document.location.href); -  const query: any = URI.parseQuery(url.query()); - -  let proposalId; -  try { -    proposalId = JSON.parse(query.proposalId); -  } catch  { -    // ignore error -  } -  const sessionId = query.sessionId; -  const contractUrl = query.contractUrl; -  const resourceUrl = query.resourceUrl; - -  ReactDOM.render( -    <ContractPrompt {...{ proposalId, contractUrl, sessionId, resourceUrl }}/>, -    document.getElementById("contract")!); -}); diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx deleted file mode 100644 index 2d4f41dfe..000000000 --- a/src/webex/pages/confirm-create-reserve.tsx +++ /dev/null @@ -1,526 +0,0 @@ -/* - This file is part of TALER - (C) 2015-2016 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 <http://www.gnu.org/licenses/> - */ - - -/** - * Page shown to the user to confirm creation - * of a reserve, usually requested by the bank. - * - * @author Florian Dold - */ - -import { canonicalizeBaseUrl } from "../../helpers"; -import * as i18n from "../../i18n"; - -import { AmountJson } from "../../amounts"; -import * as Amounts from "../../amounts"; - -import { -  CurrencyRecord, -} from "../../dbTypes"; -import { -  CreateReserveResponse, -  ReserveCreationInfo, -} from "../../walletTypes"; - -import { ImplicitStateComponent, StateHolder } from "../components"; -import { -  WalletApiError, -  createReserve, -  getCurrency, -  getExchangeInfo, -  getReserveCreationInfo, -} from "../wxApi"; - -import { -  WithdrawDetailView, -  renderAmount, -} from "../renderHtml"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - - -function delay<T>(delayMs: number, value: T): Promise<T> { -  return new Promise<T>((resolve, reject) => { -    setTimeout(() => resolve(value), delayMs); -  }); -} - -class EventTrigger { -  private triggerResolve: any; -  private triggerPromise: Promise<boolean>; - -  constructor() { -    this.reset(); -  } - -  private reset() { -    this.triggerPromise = new Promise<boolean>((resolve, reject) => { -      this.triggerResolve = resolve; -    }); -  } - -  trigger() { -    this.triggerResolve(false); -    this.reset(); -  } - -  async wait(delayMs: number): Promise<boolean> { -    return await Promise.race([this.triggerPromise, delay(delayMs, true)]); -  } -} - - -interface ExchangeSelectionProps { -  suggestedExchangeUrl: string; -  amount: AmountJson; -  callback_url: string; -  wt_types: string[]; -  currencyRecord: CurrencyRecord|null; -  sender_wire: string | undefined; -} - -interface ManualSelectionProps { -  onSelect(url: string): void; -  initialUrl: string; -} - -class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> { -  private url: StateHolder<string> = this.makeState(""); -  private errorMessage: StateHolder<string|null> = this.makeState(null); -  private isOkay: StateHolder<boolean> = this.makeState(false); -  private updateEvent = new EventTrigger(); -  constructor(p: ManualSelectionProps) { -    super(p); -    this.url(p.initialUrl); -    this.update(); -  } -  render() { -    return ( -      <div className="pure-g pure-form pure-form-stacked"> -        <div className="pure-u-1"> -          <label>URL</label> -          <input className="url" type="text" spellCheck={false} -                 value={this.url()} -                 key="exchange-url-input" -                 onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)} -                 onChange={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)} /> -        </div> -        <div className="pure-u-1"> -          <button className="pure-button button-success" -                  disabled={!this.isOkay()} -                  onClick={() => this.props.onSelect(this.url())}> -            {i18n.str`Select`} -          </button> -          <span> </span> -          {this.errorMessage()} -        </div> -      </div> -    ); -  } - -  async update() { -    this.errorMessage(null); -    this.isOkay(false); -    if (!this.url()) { -      return; -    } -    const parsedUrl = new URI(this.url()!); -    if (parsedUrl.is("relative")) { -      this.errorMessage(i18n.str`Error: URL may not be relative`); -      this.isOkay(false); -      return; -    } -    try { -      const url = canonicalizeBaseUrl(this.url()!); -      await getExchangeInfo(url); -      console.log("getExchangeInfo returned"); -      this.isOkay(true); -    } catch (e) { -      if (!(e instanceof WalletApiError)) { -        // maybe it's something more serious, don't handle here! -        throw e; -      } -      console.log(`got error "${e.message} "with detail`, e.detail); -      this.errorMessage(i18n.str`Invalid exchange URL (${e.message})`); -    } -  } - -  async onUrlChanged(s: string) { -    this.url(s); -    this.errorMessage(null); -    this.isOkay(false); -    this.updateEvent.trigger(); -    const waited = await this.updateEvent.wait(200); -    if (waited) { -      // Run the actual update if nobody else preempted us. -      this.update(); -    } -  } -} - - -class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { -  private statusString: StateHolder<string|null> = this.makeState(null); -  private reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState( -    null); -  private url: StateHolder<string|null> = this.makeState(null); - -  private selectingExchange: StateHolder<boolean> = this.makeState(false); - -  constructor(props: ExchangeSelectionProps) { -    super(props); -    const prefilledExchangesUrls = []; -    if (props.currencyRecord) { -      const exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); -      prefilledExchangesUrls.push(...exchanges); -    } -    if (props.suggestedExchangeUrl) { -      prefilledExchangesUrls.push(props.suggestedExchangeUrl); -    } -    if (prefilledExchangesUrls.length !== 0) { -      this.url(prefilledExchangesUrls[0]); -      this.forceReserveUpdate(); -    } else { -      this.selectingExchange(true); -    } -  } - -  renderFeeStatus() { -    const rci = this.reserveCreationInfo(); -    if (rci) { -      const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; -      let trustMessage; -      if (rci.isTrusted) { -        trustMessage = ( -          <i18n.Translate wrap="p"> -            The exchange is trusted by the wallet. -          </i18n.Translate> -        ); -      } else if (rci.isAudited) { -        trustMessage = ( -          <i18n.Translate wrap="p"> -            The exchange is audited by a trusted auditor. -          </i18n.Translate> -        ); -      } else { -        trustMessage = ( -          <i18n.Translate wrap="p"> -            Warning:  The exchange is neither directly trusted nor audited by a trusted auditor. -            If you withdraw from this exchange, it will be trusted in the future. -          </i18n.Translate> -        ); -      } -      return ( -        <div> -        <i18n.Translate wrap="p"> -          Using exchange provider <strong>{this.url()}</strong>. -          The exchange provider will charge -          {" "}<span>{renderAmount(totalCost)}</span>{" "} -          in fees. -        </i18n.Translate> -        {trustMessage} -        </div> -      ); -    } -    if (this.url() && !this.statusString()) { -      const shortName = new URI(this.url()!).host(); -      return ( -        <i18n.Translate wrap="p"> -          Waiting for a response from -          <span> </span> -          <em>{shortName}</em> -        </i18n.Translate> -      ); -    } -    if (this.statusString()) { -      return ( -        <p> -          <strong style={{color: "red"}}>{this.statusString()}</strong> -        </p> -      ); -    } -    return ( -      <p> -        {i18n.str`Information about fees will be available when an exchange provider is selected.`} -      </p> -    ); -  } - -  renderUpdateStatus() { -    const rci = this.reserveCreationInfo(); -    if (!rci) { -      return null; -    } -    if (!rci.versionMatch) { -      return null; -    } -    if (rci.versionMatch.compatible) { -      return null; -    } -    if (rci.versionMatch.currentCmp === -1) { -      return ( -        <p className="errorbox"> -          <i18n.Translate wrap="span"> -          Your wallet (protocol version <span>{rci.walletVersion}</span>) might be outdated.<span> </span> -          The exchange has a higher, incompatible -          protocol version (<span>{rci.exchangeVersion}</span>). -          </i18n.Translate> -        </p> -      ); -    } -    if (rci.versionMatch.currentCmp === 1) { -      return ( -        <p className="errorbox"> -          <i18n.Translate wrap="span"> -          The chosen exchange (protocol version <span>{rci.exchangeVersion}</span> might be outdated.<span> </span> -          The exchange has a lower, incompatible -          protocol version than your wallet (protocol version <span>{rci.walletVersion}</span>). -          </i18n.Translate> -        </p> -      ); -    } -    throw Error("not reached"); -  } - -  renderConfirm() { -    return ( -      <div> -        {this.renderFeeStatus()} -        <p> -        <button className="pure-button button-success" -                disabled={this.reserveCreationInfo() === null} -                onClick={() => this.confirmReserve()}> -          {i18n.str`Accept fees and withdraw`} -        </button> -        { " " } -        <button className="pure-button button-secondary" -                onClick={() => this.selectingExchange(true)}> -          {i18n.str`Change Exchange Provider`} -        </button> -        </p> -        {this.renderUpdateStatus()} -        <WithdrawDetailView rci={this.reserveCreationInfo()} /> -      </div> -    ); -  } - -  select(url: string) { -    this.reserveCreationInfo(null); -    this.url(url); -    this.selectingExchange(false); -    this.forceReserveUpdate(); -  } - -  renderSelect() { -    const exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || []; -    console.log(exchanges); -    return ( -      <div> -        {i18n.str`Please select an exchange.  You can review the details before after your selection.`} - -        {this.props.suggestedExchangeUrl && ( -          <div> -            <h2>Bank Suggestion</h2> -            <button className="pure-button button-success" onClick={() => this.select(this.props.suggestedExchangeUrl)}> -              <i18n.Translate wrap="span"> -              Select <strong>{this.props.suggestedExchangeUrl}</strong> -              </i18n.Translate> -            </button> -          </div> -        )} - -        {exchanges.length > 0 && ( -          <div> -            <h2>Known Exchanges</h2> -            {exchanges.map((e) => ( -              <button key={e.baseUrl} className="pure-button button-success" onClick={() => this.select(e.baseUrl)}> -                <i18n.Translate> -                Select <strong>{e.baseUrl}</strong> -                </i18n.Translate> -              </button> -            ))} -          </div> -        )} - -        <h2>i18n.str`Manual Selection`</h2> -        <ManualSelection initialUrl={this.url() || ""} onSelect={(url: string) => this.select(url)} /> -      </div> -    ); -  } - -  render(): JSX.Element { -    return ( -      <div> -        <i18n.Translate wrap="p"> -          You are about to withdraw -          {" "}<strong>{renderAmount(this.props.amount)}</strong>{" "} -          from your bank account into your wallet. -        </i18n.Translate> -        {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()} -      </div> -    ); -  } - - -  confirmReserve() { -    this.confirmReserveImpl(this.reserveCreationInfo()!, -                            this.url()!, -                            this.props.amount, -                            this.props.callback_url, -                            this.props.sender_wire); -  } - -  /** -   * Do an update of the reserve creation info, without any debouncing. -   */ -  async forceReserveUpdate() { -    this.reserveCreationInfo(null); -    try { -      const url = canonicalizeBaseUrl(this.url()!); -      const r = await getReserveCreationInfo(url, -                                           this.props.amount); -      console.log("get exchange info resolved"); -      this.reserveCreationInfo(r); -      console.dir(r); -    } catch (e) { -      console.log("get exchange info rejected", e); -      this.statusString(`Error: ${e.message}`); -      // Re-try every 5 seconds as long as there is a problem -      setTimeout(() => this.statusString() ? this.forceReserveUpdate() : undefined, 5000); -    } -  } - -  async confirmReserveImpl(rci: ReserveCreationInfo, -                           exchange: string, -                           amount: AmountJson, -                           callback_url: string, -                           sender_wire: string | undefined) { -    const rawResp = await createReserve({ -      amount, -      exchange: canonicalizeBaseUrl(exchange), -      senderWire: sender_wire, -    }); -    if (!rawResp) { -      throw Error("empty response"); -    } -    // FIXME: filter out types that bank/exchange don't have in common -    const exchangeWireAccounts = []; - -    for (let acct of rci.exchangeWireAccounts) { -      const payto = new URI(acct); -      if (payto.scheme() != "payto") { -        console.warn("unknown wire account URI scheme", acct); -        continue; -      } -      if (this.props.wt_types.includes(payto.authority())) { -        exchangeWireAccounts.push(acct); -      } -    } - -    const chosenAcct = exchangeWireAccounts[0]; - -    if (!chosenAcct) { -      throw Error("no exchange account matches the bank's supported types"); -    } - -    if (!rawResp.error) { -      const resp = CreateReserveResponse.checked(rawResp); -      const q: {[name: string]: string|number} = { -        amount_currency: amount.currency, -        amount_fraction: amount.fraction, -        amount_value: amount.value, -        exchange: resp.exchange, -        exchange_wire_details: chosenAcct, -        reserve_pub: resp.reservePub, -      }; -      const url = new URI(callback_url).addQuery(q); -      if (!url.is("absolute")) { -        throw Error("callback url is not absolute"); -      } -      console.log("going to", url.href()); -      document.location.href = url.href(); -    } else { -      this.statusString( -        i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); -    } -  } - -  renderStatus(): any { -    if (this.statusString()) { -      return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>; -    } else if (!this.reserveCreationInfo()) { -      return <p>{i18n.str`Checking URL, please wait ...`}</p>; -    } -    return ""; -  } -} - -async function main() { -  try { -    const url = new URI(document.location.href); -    const query: any = URI.parseQuery(url.query()); -    let amount; -    try { -      amount = AmountJson.checked(JSON.parse(query.amount)); -    } catch (e) { -      throw Error(i18n.str`Can't parse amount: ${e.message}`); -    } -    const callback_url = query.callback_url; -    let wt_types; -    try { -      wt_types = JSON.parse(query.wt_types); -    } catch (e) { -      throw Error(i18n.str`Can't parse wire_types: ${e.message}`); -    } - -    let sender_wire; -    if (query.sender_wire) { -      let senderWireUri = new URI(query.sender_wire); -      if (senderWireUri.scheme() != "payto") { -        throw Error("sender wire info must be a payto URI"); -      } -      sender_wire = query.sender_wire; -    } - -    const suggestedExchangeUrl = query.suggested_exchange_url; -    const currencyRecord = await getCurrency(amount.currency); - -    const args = { -      amount, -      callback_url, -      currencyRecord, -      sender_wire, -      suggestedExchangeUrl, -      wt_types, -    }; - -    ReactDOM.render(<ExchangeSelection {...args} />, document.getElementById( -      "exchange-selection")!); - -  } catch (e) { -    // TODO: provide more context information, maybe factor it out into a -    // TODO:generic error reporting function or component. -    document.body.innerText = i18n.str`Fatal error: "${e.message}".`; -    console.error("got error", e); -  } -} - -document.addEventListener("DOMContentLoaded", () => { -  main(); -}); diff --git a/src/webex/pages/confirm-contract.html b/src/webex/pages/pay.html index 5a949159a..d3bf992ad 100644 --- a/src/webex/pages/confirm-contract.html +++ b/src/webex/pages/pay.html @@ -11,7 +11,7 @@    <link rel="icon" href="/img/icon.png">    <script src="/dist/page-common-bundle.js"></script> -  <script src="/dist/confirm-contract-bundle.js"></script> +  <script src="/dist/pay-bundle.js"></script>    <style>      button.accept { diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx new file mode 100644 index 000000000..d929426c4 --- /dev/null +++ b/src/webex/pages/pay.tsx @@ -0,0 +1,173 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * 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, PreparePayResult } from "../../walletTypes"; + +import { renderAmount } from "../renderHtml"; +import * as wxApi from "../wxApi"; + +import React, { useState, useEffect } from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); +import { WalletApiError } from "../wxApi"; + +import * as Amounts from "../../amounts"; + +function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) { +  const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(); +  const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); +  const [numTries, setNumTries] = useState(0); +  let totalFees: Amounts.AmountJson | undefined = undefined; + +  useEffect(() => { +    const doFetch = async () => { +      const p = await wxApi.preparePay(talerPayUri); +      setPayStatus(p); +    }; +    doFetch(); +  }); + +  if (!payStatus) { +    return <span>Loading payment information ...</span>; +  } + +  if (payStatus.status === "error") { +    return <span>Error: {payStatus.error}</span>; +  } + +  if (payStatus.status === "payment-possible") { +    totalFees = payStatus.totalFees; +  } + +  if (payStatus.status === "paid" && numTries === 0) { +    return ( +      <span> +        You have already paid for this article. Click{" "} +        <a href={payStatus.nextUrl}>here</a> to view it again. +      </span> +    ); +  } + +  const contractTerms = payStatus.contractTerms; + +  if (!contractTerms) { +    return ( +      <span> +        Error: did not get contract terms from merchant or wallet backend. +      </span> +    ); +  } + +  let merchantName: React.ReactElement; +  if (contractTerms.merchant && contractTerms.merchant.name) { +    merchantName = <strong>{contractTerms.merchant.name}</strong>; +  } else { +    merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>; +  } + +  const amount = ( +    <strong>{renderAmount(Amounts.parseOrThrow(contractTerms.amount))}</strong> +  ); + +  const doPayment = async () => { +    setNumTries(numTries + 1); +    try { +      const res = await wxApi.confirmPay(payStatus!.proposalId!, undefined); +      document.location.href = res.nextUrl; +    } catch (e) { +      console.error(e); +      setPayErrMsg(e.message); +    } +  }; + +  return ( +    <div> +      <p> +        <i18n.Translate wrap="p"> +          The merchant <span>{merchantName}</span> offers you to purchase: +        </i18n.Translate> +        <div style={{ textAlign: "center" }}> +          <strong>{contractTerms.summary}</strong> +        </div> +        {totalFees ? ( +          <i18n.Translate wrap="p"> +            The total price is <span>{amount} </span> +            (plus <span>{renderAmount(totalFees)}</span> fees). +          </i18n.Translate> +        ) : ( +          <i18n.Translate wrap="p"> +            The total price is <span>{amount}</span>. +          </i18n.Translate> +        )} +      </p> + +      {payErrMsg ? ( +        <div> +          <p>Payment failed: {payErrMsg}</p> +          <button +            className="pure-button button-success" +            onClick={() => doPayment()} +          > +            {i18n.str`Retry`} +          </button> +        </div> +      ) : ( +        <div> +          <button +            className="pure-button button-success" +            onClick={() => doPayment()} +          > +            {i18n.str`Confirm payment`} +          </button> +        </div> +      )} +    </div> +  ); +} + +runOnceWhenReady(() => { +  try { +    const url = new URI(document.location.href); +    const query: any = URI.parseQuery(url.query()); + +    let talerPayUri = query.talerPayUri; + +    ReactDOM.render( +      <TalerPayDialog talerPayUri={talerPayUri} />, +      document.getElementById("contract")!, +    ); +  } catch (e) { +    ReactDOM.render( +      <span>Fatal error: {e.message}</span>, +      document.getElementById("contract")!, +    ); +    console.error(e); +  } +}); diff --git a/src/webex/pages/confirm-create-reserve.html b/src/webex/pages/withdraw.html index 17daf4dde..8b1e59b1d 100644 --- a/src/webex/pages/confirm-create-reserve.html +++ b/src/webex/pages/withdraw.html @@ -10,7 +10,7 @@    <link rel="stylesheet" type="text/css" href="../style/wallet.css">    <script src="/dist/page-common-bundle.js"></script> -  <script src="/dist/confirm-create-reserve-bundle.js"></script> +  <script src="/dist/withdraw-bundle.js"></script>  </head> diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx new file mode 100644 index 000000000..66617373b --- /dev/null +++ b/src/webex/pages/withdraw.tsx @@ -0,0 +1,231 @@ +/* + This file is part of TALER + (C) 2015-2016 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 <http://www.gnu.org/licenses/> + */ + +/** + * Page shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import { canonicalizeBaseUrl } from "../../helpers"; +import * as i18n from "../../i18n"; + +import { AmountJson } from "../../amounts"; +import * as Amounts from "../../amounts"; + +import { CurrencyRecord } from "../../dbTypes"; +import { +  CreateReserveResponse, +  ReserveCreationInfo, +  WithdrawDetails, +} from "../../walletTypes"; + +import { ImplicitStateComponent, StateHolder } from "../components"; + +import { WithdrawDetailView, renderAmount } from "../renderHtml"; + +import React, { useState, useEffect } from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); +import { getWithdrawDetails, acceptWithdrawal } from "../wxApi"; + +function NewExchangeSelection(props: { talerWithdrawUri: string }) { +  const [details, setDetails] = useState<WithdrawDetails | undefined>(); +  const [selectedExchange, setSelectedExchange] = useState< +    string | undefined +  >(); +  const talerWithdrawUri = props.talerWithdrawUri; +  const [cancelled, setCancelled] = useState(false); +  const [selecting, setSelecting] = useState(false); +  const [customUrl, setCustomUrl] = useState<string>(""); +  const [errMsg, setErrMsg] = useState<string | undefined>(""); + +  useEffect(() => { +    const fetchData = async () => { +      console.log("getting from", talerWithdrawUri); +      let d: WithdrawDetails | undefined = undefined; +      try { +        d = await getWithdrawDetails(talerWithdrawUri, selectedExchange); +      } catch (e) { +        console.error("error getting withdraw details", e); +        setErrMsg(e.message); +        return; +      } +      console.log("got withdrawDetails", d); +      if (!selectedExchange && d.withdrawInfo.suggestedExchange) { +        console.log("setting selected exchange"); +        setSelectedExchange(d.withdrawInfo.suggestedExchange); +      } +      setDetails(d); +    }; +    fetchData(); +  }, [selectedExchange, errMsg, selecting]); + +  if (errMsg) { +    return ( +      <div> +        <i18n.Translate wrap="p"> +          Could not get details for withdraw operation: +        </i18n.Translate> +        <p style={{ color: "red" }}>{errMsg}</p> +        <p> +          <span +            role="button" +            tabIndex={0} +            style={{ textDecoration: "underline", cursor: "pointer" }} +            onClick={() => { +              setSelecting(true); +              setErrMsg(undefined); +              setSelectedExchange(undefined); +              setDetails(undefined); +            }} +          > +            {i18n.str`Chose different exchange provider`} +          </span> +        </p> +      </div> +    ); +  } + +  if (!details) { +    return <span>Loading...</span>; +  } + +  if (cancelled) { +    return <span>Withdraw operation has been cancelled.</span>; +  } + +  if (selecting) { +    const bankSuggestion = details && details.withdrawInfo.suggestedExchange; +    return ( +      <div> +        {i18n.str`Please select an exchange.  You can review the details before after your selection.`} +        {bankSuggestion && ( +          <div> +            <h2>Bank Suggestion</h2> +            <button +              className="pure-button button-success" +              onClick={() => { +                setDetails(undefined); +                setSelectedExchange(bankSuggestion); +                setSelecting(false); +              }} +            > +              <i18n.Translate wrap="span"> +                Select <strong>{bankSuggestion}</strong> +              </i18n.Translate> +            </button> +          </div> +        )} +        <h2>Custom Selection</h2> +        <p> +          <input +            type="text" +            onChange={e => setCustomUrl(e.target.value)} +            value={customUrl} +          /> +        </p> +        <button +          className="pure-button button-success" +          onClick={() => { +            setDetails(undefined); +            setSelectedExchange(customUrl); +            setSelecting(false); +          }} +        > +          <i18n.Translate wrap="span">Select custom exchange</i18n.Translate> +        </button> +      </div> +    ); +  } + +  const accept = async () => { +    console.log("accepting exchange", selectedExchange); +    const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange!); +    console.log("accept withdrawal response", res); +    if (res.confirmTransferUrl) { +      document.location.href = res.confirmTransferUrl; +    } +  }; + +  return ( +    <div> +      <i18n.Translate wrap="p"> +        You are about to withdraw{" "} +        <strong>{renderAmount(details.withdrawInfo.amount)}</strong> from your +        bank account into your wallet. +      </i18n.Translate> +      <div> +        <button +          className="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> + +        {details.reserveCreationInfo ? ( +          <WithdrawDetailView rci={details.reserveCreationInfo} /> +        ) : null} +      </div> +    </div> +  ); +} + +async function main() { +  try { +    const url = new URI(document.location.href); +    const query: any = URI.parseQuery(url.query()); +    let talerWithdrawUri = query.talerWithdrawUri; +    if (!talerWithdrawUri) { +      throw Error("withdraw URI required"); +    } + +    ReactDOM.render( +      <NewExchangeSelection talerWithdrawUri={talerWithdrawUri} />, +      document.getElementById("exchange-selection")!, +    ); +  } catch (e) { +    // TODO: provide more context information, maybe factor it out into a +    // TODO:generic error reporting function or component. +    document.body.innerText = i18n.str`Fatal error: "${e.message}".`; +    console.error("got error", e); +  } +} + +document.addEventListener("DOMContentLoaded", () => { +  main(); +}); diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css index dde17e890..b4bfd6f6d 100644 --- a/src/webex/style/wallet.css +++ b/src/webex/style/wallet.css @@ -137,6 +137,11 @@ button.linky {      cursor:pointer;  } +.blacklink a:link, .blacklink a:visited, .blacklink a:hover, .blacklink a:active { +  color: #000; +} + +  table, th, td {      border: 1px solid black;  } diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 4f7500368..feabc7819 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -79,6 +79,8 @@ export interface UpgradeResponse {  export class WalletApiError extends Error {    constructor(message: string, public detail: any) {      super(message); +    // restore prototype chain +    Object.setPrototypeOf(this, new.target.prototype);    }  } @@ -401,3 +403,24 @@ export function abortFailedPayment(contractTermsHash: string) {  export function benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {    return callBackend("benchmark-crypto", { repetitions });  } + +/** + * Get details about a withdraw operation. + */ +export function getWithdrawDetails(talerWithdrawUri: string, maybeSelectedExchange: string | undefined) { +  return callBackend("get-withdraw-details", { talerWithdrawUri, maybeSelectedExchange }); +} + +/** + * Get details about a pay operation. + */ +export function preparePay(talerPayUri: string) { +  return callBackend("prepare-pay", { talerPayUri }); +} + +/** + * Get details about a withdraw operation. + */ +export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) { +  return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange }); +}
\ No newline at end of file diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 594418ebf..d31ea388d 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -339,6 +339,20 @@ function handleMessage(        }        return needsWallet().benchmarkCrypto(detail.repetitions);      } +    case "get-withdraw-details": { +      return needsWallet().getWithdrawDetails( +        detail.talerWithdrawUri, +        detail.maybeSelectedExchange, +      ); +    } +    case "accept-withdrawal": { +      return needsWallet().acceptWithdrawal( +        detail.talerWithdrawUri, +        detail.selectedExchange, +      ); +    } +    case "prepare-pay": +      return needsWallet().preparePay(detail.talerPayUri);      default:        // Exhaustiveness check.        // See https://www.typescriptlang.org/docs/handbook/advanced-types.html @@ -523,190 +537,6 @@ function makeSyncWalletRedirect(    return { redirectUrl: outerUrl.href() };  } -/** - * Handle a HTTP response that has the "402 Payment Required" status. - * In this callback we don't have access to the body, and must communicate via - * shared state with the content script that will later be run later - * in this tab. - */ -function handleHttpPayment( -  headerList: chrome.webRequest.HttpHeader[], -  url: string, -  tabId: number, -): any { -  if (!currentWallet) { -    console.log("can't handle payment, no wallet"); -    return; -  } - -  const headers: { [s: string]: string } = {}; -  for (const kv of headerList) { -    if (kv.value) { -      headers[kv.name.toLowerCase()] = kv.value; -    } -  } - -  const decodeIfDefined = (url?: string) => -    url ? decodeURIComponent(url) : undefined; - -  const fields = { -    contract_url: decodeIfDefined(headers["taler-contract-url"]), -    offer_url: decodeIfDefined(headers["taler-offer-url"]), -    refund_url: decodeIfDefined(headers["taler-refund-url"]), -    resource_url: decodeIfDefined(headers["taler-resource-url"]), -    session_id: decodeIfDefined(headers["taler-session-id"]), -    tip: decodeIfDefined(headers["taler-tip"]), -  }; - -  const talerHeaderFound = -    Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0; - -  if (!talerHeaderFound) { -    // looks like it's not a taler request, it might be -    // for a different payment system (or the shop is buggy) -    console.log("ignoring non-taler 402 response"); -    return; -  } - -  console.log("got pay detail", fields); - -  // Synchronous fast path for existing payment -  if (fields.resource_url) { -    const result = currentWallet.getNextUrlFromResourceUrl(fields.resource_url); -    if ( -      result && -      (fields.session_id === undefined || -        fields.session_id === result.lastSessionId) -    ) { -      return { redirectUrl: result.nextUrl }; -    } -  } -  // Synchronous fast path for new contract -  if (fields.contract_url) { -    return makeSyncWalletRedirect("confirm-contract.html", tabId, url, { -      contractUrl: fields.contract_url, -      resourceUrl: fields.resource_url, -      sessionId: fields.session_id, -    }); -  } - -  // Synchronous fast path for tip -  if (fields.tip) { -    return makeSyncWalletRedirect("tip.html", tabId, url, { -      tip_token: fields.tip, -    }); -  } - -  // Synchronous fast path for refund -  if (fields.refund_url) { -    console.log("processing refund"); -    return makeSyncWalletRedirect("refund.html", tabId, url, { -      refundUrl: fields.refund_url, -    }); -  } - -  // We need to do some asynchronous operation, we can't directly redirect -  talerPay(fields, url, tabId).then(nextUrl => { -    if (nextUrl) { -      // We use chrome.tabs.executeScript instead of chrome.tabs.update -      // because the latter is buggy when it does not execute in the same -      // (micro-?)task as the header callback. -      chrome.tabs.executeScript({ -        code: `document.location.href = decodeURIComponent("${encodeURI( -          nextUrl, -        )}");`, -        runAt: "document_start", -      }); -    } -  }); - -  return; -} - -function handleBankRequest( -  wallet: Wallet, -  headerList: chrome.webRequest.HttpHeader[], -  url: string, -  tabId: number, -): any { -  const headers: { [s: string]: string } = {}; -  for (const kv of headerList) { -    if (kv.value) { -      headers[kv.name.toLowerCase()] = kv.value; -    } -  } - -  const operation = headers["taler-operation"]; - -  if (!operation) { -    // Not a taler related request. -    return; -  } - -  if (operation === "confirm-reserve") { -    const reservePub = headers["taler-reserve-pub"]; -    if (reservePub !== undefined) { -      console.log(`confirming reserve ${reservePub} via 201`); -      wallet.confirmReserve({ reservePub }); -    } else { -      console.warn( -        "got 'Taler-Operation: confirm-reserve' without 'Taler-Reserve-Pub'", -      ); -    } -    return; -  } - -  if (operation === "create-reserve") { -    const amount = headers["taler-amount"]; -    if (!amount) { -      console.log("202 not understood (Taler-Amount missing)"); -      return; -    } -    const callbackUrl = headers["taler-callback-url"]; -    if (!callbackUrl) { -      console.log("202 not understood (Taler-Callback-Url missing)"); -      return; -    } -    try { -      JSON.parse(amount); -    } catch (e) { -      const errUri = new URI( -        chrome.extension.getURL("/src/webex/pages/error.html"), -      ); -      const p = { -        message: `Can't parse amount ("${amount}"): ${e.message}`, -      }; -      const errRedirectUrl = errUri.query(p).href(); -      // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed -      chrome.tabs.update(tabId, { url: errRedirectUrl }); -      return; -    } -    const wtTypes = headers["taler-wt-types"]; -    if (!wtTypes) { -      console.log("202 not understood (Taler-Wt-Types missing)"); -      return; -    } -    const params = { -      amount, -      bank_url: url, -      callback_url: new URI(callbackUrl).absoluteTo(url), -      sender_wire: headers["taler-sender-wire"], -      suggested_exchange_url: headers["taler-suggested-exchange"], -      wt_types: wtTypes, -    }; -    const uri = new URI( -      chrome.extension.getURL("/src/webex/pages/confirm-create-reserve.html"), -    ); -    const redirectUrl = uri.query(params).href(); -    console.log("redirecting to", redirectUrl); -    // FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed -    chrome.tabs.update(tabId, { url: redirectUrl }); -    return; -  } - -  console.log("Ignoring unknown (X-)Taler-Operation:", operation); -} -  // Rate limit cache for executePayment operations, to break redirect loops  let rateLimitCache: { [n: number]: number } = {}; @@ -931,19 +761,59 @@ export async function wxMain() {        }        if (details.statusCode === 402) {          console.log(`got 402 from ${details.url}`); -        return handleHttpPayment( -          details.responseHeaders || [], -          details.url, -          details.tabId, -        ); -      } else if (details.statusCode === 202) { -        return handleBankRequest( -          wallet!, -          details.responseHeaders || [], -          details.url, -          details.tabId, -        ); +        for (let header of details.responseHeaders || []) { +          if (header.name.toLowerCase() === "taler") { +            const talerUri = header.value || ""; +            if (!talerUri.startsWith("taler://")) { +              console.warn( +                "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", +              ); +              break; +            } +            if (talerUri.startsWith("taler://withdraw/")) { +              return makeSyncWalletRedirect( +                "withdraw.html", +                details.tabId, +                details.url, +                { +                  talerWithdrawUri: talerUri, +                }, +              ); +            } else if (talerUri.startsWith("taler://pay/")) { +              return makeSyncWalletRedirect( +                "pay.html", +                details.tabId, +                details.url, +                { +                  talerPayUri: talerUri, +                }, +              ); +            } else if (talerUri.startsWith("taler://tip/")) { +              return makeSyncWalletRedirect( +                "tip.html", +                details.tabId, +                details.url, +                { +                  talerTipUri: talerUri, +                }, +              ); +            } else if (talerUri.startsWith("taler://refund/")) { +              return makeSyncWalletRedirect( +                "refund.html", +                details.tabId, +                details.url, +                { +                  talerRefundUri: talerUri, +                }, +              ); +            } else { +              console.warn("Unknown action in taler:// URI, ignoring."); +            } +            break; +          } +        }        } +      return {};      },      { urls: ["<all_urls>"] },      ["responseHeaders", "blocking"], | 
